ifnex/04_Laravel/app/Services/ShipmentReviewService.php
Kazem Alghasi 7faa778e22 refactor(api): update shipment review logic to update existing reviews
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
2026-09-29 04:37:55 +03:30

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;
}
}