Refactor `ShipmentReviewService` to update the existing pending review instead of creating a new `ShipmentReview` record during the approval and rejection processes. This ensures that the review lifecycle correctly finalizes the current pending revision. - Implement `getPendingReview` to retrieve the active pending review - Update `approve` to modify the existing review record - Update `reject` to modify the existing review record - Add documentation clarifying when new revisions are created versus when existing ones are finalized
207 lines
6.2 KiB
PHP
207 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Enums\ReviewState;
|
|
use App\Enums\ShipmentStatus;
|
|
use App\Models\Shipment;
|
|
use App\Models\ShipmentReview;
|
|
use App\Models\ShipmentStatusHistory;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use RuntimeException;
|
|
|
|
class ShipmentReviewService
|
|
{
|
|
/**
|
|
* درخواست اصلاح سفارش توسط کارمند.
|
|
*
|
|
* این عملیات یک Review Revision جدید ایجاد میکند،
|
|
* چون یک submission جدید برای بررسی ثبت شده است.
|
|
*/
|
|
public function requestChanges(
|
|
Shipment $shipment,
|
|
User $user,
|
|
string $reason,
|
|
?string $notes = null
|
|
): Shipment {
|
|
return DB::transaction(function () use ($shipment, $user, $reason, $notes) {
|
|
|
|
$shipment = Shipment::query()
|
|
->lockForUpdate()
|
|
->findOrFail($shipment->id);
|
|
|
|
$this->assertPendingReview(
|
|
$shipment,
|
|
'این سفارش در وضعیت قابل درخواست اصلاح نیست.'
|
|
);
|
|
|
|
$revisionNo = $this->nextReviewRevision($shipment);
|
|
|
|
$shipment->update([
|
|
'review_state' => ReviewState::ChangesRequested,
|
|
]);
|
|
|
|
ShipmentReview::create([
|
|
'shipment_id' => $shipment->id,
|
|
'revision_no' => $revisionNo,
|
|
'decision' => ReviewState::ChangesRequested->value,
|
|
'reason' => $reason,
|
|
'notes' => $notes,
|
|
'reviewed_by' => $user->id,
|
|
'reviewed_at' => now(),
|
|
]);
|
|
|
|
return $shipment->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* تأیید سفارش توسط کارمند.
|
|
*
|
|
* تأیید، Review جدید ایجاد نمیکند.
|
|
* آخرین revision با وضعیت pending را finalize میکند.
|
|
*/
|
|
public function approve(
|
|
Shipment $shipment,
|
|
User $user,
|
|
?string $notes = null
|
|
): Shipment {
|
|
return DB::transaction(function () use ($shipment, $user, $notes) {
|
|
|
|
$shipment = Shipment::query()
|
|
->lockForUpdate()
|
|
->findOrFail($shipment->id);
|
|
|
|
$this->assertPendingReview(
|
|
$shipment,
|
|
'این سفارش در وضعیت قابل تأیید نیست.'
|
|
);
|
|
|
|
$review = $this->getPendingReview($shipment);
|
|
|
|
$oldStatus = $shipment->status;
|
|
|
|
$shipment->update([
|
|
'status' => ShipmentStatus::Approved,
|
|
'review_state' => ReviewState::Approved,
|
|
]);
|
|
|
|
ShipmentStatusHistory::create([
|
|
'shipment_id' => $shipment->id,
|
|
'from_status' => $oldStatus->value,
|
|
'to_status' => ShipmentStatus::Approved->value,
|
|
'reason' => $notes ?: 'تأیید توسط کارمند',
|
|
'changed_by' => $user->id,
|
|
]);
|
|
|
|
$review->update([
|
|
'decision' => ReviewState::Approved->value,
|
|
'reason' => null,
|
|
'notes' => $notes,
|
|
'reviewed_by' => $user->id,
|
|
'reviewed_at' => now(),
|
|
]);
|
|
|
|
return $shipment->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* رد سفارش توسط کارمند.
|
|
*
|
|
* رد، Review جدید ایجاد نمیکند.
|
|
* آخرین revision با وضعیت pending را finalize میکند.
|
|
*/
|
|
public function reject(
|
|
Shipment $shipment,
|
|
User $user,
|
|
string $reason
|
|
): Shipment {
|
|
return DB::transaction(function () use ($shipment, $user, $reason) {
|
|
|
|
$shipment = Shipment::query()
|
|
->lockForUpdate()
|
|
->findOrFail($shipment->id);
|
|
|
|
$this->assertPendingReview(
|
|
$shipment,
|
|
'این سفارش در وضعیت قابل رد نیست.'
|
|
);
|
|
|
|
$review = $this->getPendingReview($shipment);
|
|
|
|
$oldStatus = $shipment->status;
|
|
|
|
$shipment->update([
|
|
'status' => ShipmentStatus::Cancelled,
|
|
'review_state' => ReviewState::Rejected,
|
|
]);
|
|
|
|
ShipmentStatusHistory::create([
|
|
'shipment_id' => $shipment->id,
|
|
'from_status' => $oldStatus->value,
|
|
'to_status' => ShipmentStatus::Cancelled->value,
|
|
'reason' => 'رد شده: ' . $reason,
|
|
'changed_by' => $user->id,
|
|
]);
|
|
|
|
$review->update([
|
|
'decision' => ReviewState::Rejected->value,
|
|
'reason' => $reason,
|
|
'notes' => null,
|
|
'reviewed_by' => $user->id,
|
|
'reviewed_at' => now(),
|
|
]);
|
|
|
|
return $shipment->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* بررسی وضعیت فعلی Shipment برای عملیات Review.
|
|
*/
|
|
private function assertPendingReview(
|
|
Shipment $shipment,
|
|
string $message
|
|
): void {
|
|
if (
|
|
$shipment->status !== ShipmentStatus::PendingApproval ||
|
|
$shipment->review_state !== ReviewState::Pending
|
|
) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* پیدا کردن Review فعلی که منتظر تصمیم کارمند است.
|
|
*
|
|
* بعد از resubmit باید دقیقاً یک revision با decision=pending
|
|
* وجود داشته باشد.
|
|
*/
|
|
private function getPendingReview(Shipment $shipment): ShipmentReview
|
|
{
|
|
$review = $shipment->reviews()
|
|
->where('decision', ReviewState::Pending->value)
|
|
->orderByDesc('revision_no')
|
|
->first();
|
|
|
|
if (!$review) {
|
|
throw new RuntimeException(
|
|
'برای این سفارش هیچ Review در وضعیت pending پیدا نشد.'
|
|
);
|
|
}
|
|
|
|
return $review;
|
|
}
|
|
|
|
/**
|
|
* تعیین شماره revision بعدی.
|
|
*
|
|
* هر resubmission یک revision جدید ایجاد میکند.
|
|
*/
|
|
private function nextReviewRevision(Shipment $shipment): int
|
|
{
|
|
return ((int) $shipment->reviews()->max('revision_no')) + 1;
|
|
}
|
|
} |