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(); }); } /** * تأیید سفارش توسط کارمند. */ 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, 'این سفارش در وضعیت قابل تأیید نیست.' ); $oldStatus = $shipment->status; $revisionNo = $this->nextReviewRevision($shipment); $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, ]); ShipmentReview::create([ 'shipment_id' => $shipment->id, 'revision_no' => $revisionNo, 'decision' => ReviewState::Approved->value, 'reason' => null, 'notes' => $notes, 'reviewed_by' => $user->id, 'reviewed_at' => now(), ]); return $shipment->fresh(); }); } /** * رد سفارش توسط کارمند. */ 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, 'این سفارش در وضعیت قابل رد نیست.' ); $oldStatus = $shipment->status; $revisionNo = $this->nextReviewRevision($shipment); $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, ]); ShipmentReview::create([ 'shipment_id' => $shipment->id, 'revision_no' => $revisionNo, 'decision' => ReviewState::Rejected->value, 'reason' => $reason, 'notes' => null, 'reviewed_by' => $user->id, 'reviewed_at' => now(), ]); return $shipment->fresh(); }); } /** * بررسی وضعیت فعلی برای عملیات Review. * * این check داخل transaction و بعد از lock انجام می‌شود * تا race condition بین دو کارمند کاهش یابد. */ private function assertPendingReview( Shipment $shipment, string $message ): void { if ( $shipment->status !== ShipmentStatus::PendingApproval || $shipment->review_state !== ReviewState::Pending ) { throw new RuntimeException($message); } } /** * تعیین revision بعدی. */ private function nextReviewRevision(Shipment $shipment): int { return ((int) $shipment->reviews()->max('revision_no')) + 1; } }