ifnex/04_Laravel/app/Services/ShipmentReviewService.php
Kazem Alghasi b9b83ffecf feat(shipment): implement checklist templating and automated instantiation
Introduce a template-based system for shipment checklists to automate
the creation of required inspection items upon shipment approval.

- Add `ShipmentChecklistTemplate` model and migration to define reusable
  checklist structures.
- Extend `ShipmentChecklist` schema to support item titles, requirement
  flags, sorting, and attachment paths.
- Implement `instantiateChecklist` in `ShipmentReviewService` to
  automatically generate checklist items from active templates based on
  shipment direction (import/export).
- Refactor `ShipmentChecklistResource` and `ChecklistsRelationManager`
  in Filament to support the new data structure and improved UI.
- Update `ShipmentChecklist` model with helper methods for completion
  tracking and improved relationship handling.
2026-09-30 18:35:25 +03:30

247 lines
7.7 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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\ShipmentChecklistTemplate;
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(),
]);
// خودکارسازی چک‌لیست از قالب‌های فعال (بعد از تأیید سفارش)
$this->instantiateChecklist($shipment);
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;
}
/**
* ساخت instance‌های چک‌لیست از قالب‌های فعال متناسب با جهت سفارش.
* فقط اگه هنوز چک‌لیستی برای این سفارش ساخته نشده باشد.
*/
private function instantiateChecklist(Shipment $shipment): void
{
// جلوگیری از تکرار اگه قبلاً ساخته شده
if ($shipment->checklists()->exists()) {
return;
}
$direction = $shipment->direction->value; // 'export' | 'import'
$templates = ShipmentChecklistTemplate::query()
->where('is_active', true)
->where(function ($q) use ($direction) {
$q->where('direction', 'both')->orWhere('direction', $direction);
})
->orderBy('sort_order')
->get();
if ($templates->isEmpty()) {
return; // قالب فعالی نیست — چک‌لیست ساخته نمی‌شه
}
$rows = $templates->map(fn ($t) => [
'shipment_id' => $shipment->id,
'item_title' => $t->item_title,
'is_required' => $t->is_required,
'sort_order' => $t->sort_order,
'is_completed' => false,
])->toArray();
$shipment->checklists()->createMany($rows);
}
}