diff --git a/04_Laravel/.phpunit.result.cache b/04_Laravel/.phpunit.result.cache index 723227c..358fc76 100644 --- a/04_Laravel/.phpunit.result.cache +++ b/04_Laravel/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\Api\\PricingControllerTest::it_can_calculate_pricing_with_valid_data":5,"Tests\\Feature\\Api\\PricingControllerTest::it_returns_validation_errors_for_invalid_data":7,"Tests\\Feature\\Services\\PriceCalculatorServiceTest::it_calculates_price_correctly_for_standard_package":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_returns_active_discount_codes":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_validates_discount_code_successfully":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_completes_mock_payment_successfully":8,"Tests\\Feature\\Services\\WalletServiceTest::it_creates_deposit_transaction":8,"Tests\\Feature\\Services\\WalletServiceTest::it_completes_deposit_and_updates_balance":8,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_deposit":8,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_withdrawal":8,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_withdrawal_when_balance_is_insufficient":8,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_order_payment":8,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_order_payment_when_balance_is_insufficient":8,"Tests\\Feature\\Services\\WalletServiceTest::it_fails_transaction":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_invalid_discount_code":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_expired_discount_code":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_code_below_minimum_order_amount":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_calculates_fixed_discount_correctly":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_redirects_to_gateway_for_payment":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_validates_minimum_deposit_amount":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_prevents_payment_for_frozen_wallet":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_checks_transaction_status":8},"times":{"Tests\\Feature\\Api\\PricingControllerTest::it_can_calculate_pricing_with_valid_data":0.057,"Tests\\Feature\\Api\\PricingControllerTest::it_returns_validation_errors_for_invalid_data":0.057,"Tests\\Feature\\Services\\PriceCalculatorServiceTest::it_calculates_price_correctly_for_standard_package":0.012,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.013,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.055,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_returns_active_discount_codes":0.1,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_validates_discount_code_successfully":0.059,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_invalid_discount_code":0.048,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_expired_discount_code":0.047,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_code_below_minimum_order_amount":0.059,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_calculates_fixed_discount_correctly":0.047,"Tests\\Feature\\Api\\PaymentControllerTest::it_redirects_to_gateway_for_payment":0.081,"Tests\\Feature\\Api\\PaymentControllerTest::it_validates_minimum_deposit_amount":0.054,"Tests\\Feature\\Api\\PaymentControllerTest::it_prevents_payment_for_frozen_wallet":0.046,"Tests\\Feature\\Api\\PaymentControllerTest::it_completes_mock_payment_successfully":0.065,"Tests\\Feature\\Api\\PaymentControllerTest::it_checks_transaction_status":0.054,"Tests\\Feature\\Services\\WalletServiceTest::it_creates_deposit_transaction":0.003,"Tests\\Feature\\Services\\WalletServiceTest::it_completes_deposit_and_updates_balance":0.008,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_deposit":0.005,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_withdrawal":0.006,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_withdrawal_when_balance_is_insufficient":0.012,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_order_payment":0.004,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_order_payment_when_balance_is_insufficient":0.002,"Tests\\Feature\\Services\\WalletServiceTest::it_fails_transaction":0.005}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\Api\\PricingControllerTest::it_can_calculate_pricing_with_valid_data":5,"Tests\\Feature\\Api\\PricingControllerTest::it_returns_validation_errors_for_invalid_data":7,"Tests\\Feature\\Services\\PriceCalculatorServiceTest::it_calculates_price_correctly_for_standard_package":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_returns_active_discount_codes":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_validates_discount_code_successfully":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_completes_mock_payment_successfully":8,"Tests\\Feature\\Services\\WalletServiceTest::it_creates_deposit_transaction":8,"Tests\\Feature\\Services\\WalletServiceTest::it_completes_deposit_and_updates_balance":8,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_deposit":8,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_withdrawal":8,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_withdrawal_when_balance_is_insufficient":8,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_order_payment":8,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_order_payment_when_balance_is_insufficient":8,"Tests\\Feature\\Services\\WalletServiceTest::it_fails_transaction":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_invalid_discount_code":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_expired_discount_code":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_code_below_minimum_order_amount":8,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_calculates_fixed_discount_correctly":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_redirects_to_gateway_for_payment":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_validates_minimum_deposit_amount":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_prevents_payment_for_frozen_wallet":8,"Tests\\Feature\\Api\\PaymentControllerTest::it_checks_transaction_status":8},"times":{"Tests\\Feature\\Api\\PricingControllerTest::it_can_calculate_pricing_with_valid_data":0.33,"Tests\\Feature\\Api\\PricingControllerTest::it_returns_validation_errors_for_invalid_data":0.063,"Tests\\Feature\\Services\\PriceCalculatorServiceTest::it_calculates_price_correctly_for_standard_package":0.044,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.008,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.373,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_returns_active_discount_codes":0.429,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_validates_discount_code_successfully":0.058,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_invalid_discount_code":0.044,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_expired_discount_code":0.044,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_rejects_code_below_minimum_order_amount":0.044,"Tests\\Feature\\Api\\DiscountCodeControllerTest::it_calculates_fixed_discount_correctly":0.043,"Tests\\Feature\\Api\\PaymentControllerTest::it_redirects_to_gateway_for_payment":0.517,"Tests\\Feature\\Api\\PaymentControllerTest::it_validates_minimum_deposit_amount":0.051,"Tests\\Feature\\Api\\PaymentControllerTest::it_prevents_payment_for_frozen_wallet":0.046,"Tests\\Feature\\Api\\PaymentControllerTest::it_completes_mock_payment_successfully":0.083,"Tests\\Feature\\Api\\PaymentControllerTest::it_checks_transaction_status":0.05,"Tests\\Feature\\Services\\WalletServiceTest::it_creates_deposit_transaction":0.003,"Tests\\Feature\\Services\\WalletServiceTest::it_completes_deposit_and_updates_balance":0.008,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_deposit":0.005,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_manual_withdrawal":0.007,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_withdrawal_when_balance_is_insufficient":0.004,"Tests\\Feature\\Services\\WalletServiceTest::it_processes_order_payment":0.004,"Tests\\Feature\\Services\\WalletServiceTest::it_prevents_order_payment_when_balance_is_insufficient":0.002,"Tests\\Feature\\Services\\WalletServiceTest::it_fails_transaction":0.005}} \ No newline at end of file diff --git a/04_Laravel/app/Services/ShipmentReviewService.php b/04_Laravel/app/Services/ShipmentReviewService.php index a1bc64a..5376639 100644 --- a/04_Laravel/app/Services/ShipmentReviewService.php +++ b/04_Laravel/app/Services/ShipmentReviewService.php @@ -15,6 +15,9 @@ class ShipmentReviewService { /** * درخواست اصلاح سفارش توسط کارمند. + * + * این عملیات یک Review Revision جدید ایجاد می‌کند، + * چون یک submission جدید برای بررسی ثبت شده است. */ public function requestChanges( Shipment $shipment, @@ -55,6 +58,9 @@ class ShipmentReviewService /** * تأیید سفارش توسط کارمند. + * + * تأیید، Review جدید ایجاد نمی‌کند. + * آخرین revision با وضعیت pending را finalize می‌کند. */ public function approve( Shipment $shipment, @@ -72,8 +78,9 @@ class ShipmentReviewService 'این سفارش در وضعیت قابل تأیید نیست.' ); + $review = $this->getPendingReview($shipment); + $oldStatus = $shipment->status; - $revisionNo = $this->nextReviewRevision($shipment); $shipment->update([ 'status' => ShipmentStatus::Approved, @@ -88,9 +95,7 @@ class ShipmentReviewService 'changed_by' => $user->id, ]); - ShipmentReview::create([ - 'shipment_id' => $shipment->id, - 'revision_no' => $revisionNo, + $review->update([ 'decision' => ReviewState::Approved->value, 'reason' => null, 'notes' => $notes, @@ -104,6 +109,9 @@ class ShipmentReviewService /** * رد سفارش توسط کارمند. + * + * رد، Review جدید ایجاد نمی‌کند. + * آخرین revision با وضعیت pending را finalize می‌کند. */ public function reject( Shipment $shipment, @@ -121,8 +129,9 @@ class ShipmentReviewService 'این سفارش در وضعیت قابل رد نیست.' ); + $review = $this->getPendingReview($shipment); + $oldStatus = $shipment->status; - $revisionNo = $this->nextReviewRevision($shipment); $shipment->update([ 'status' => ShipmentStatus::Cancelled, @@ -137,9 +146,7 @@ class ShipmentReviewService 'changed_by' => $user->id, ]); - ShipmentReview::create([ - 'shipment_id' => $shipment->id, - 'revision_no' => $revisionNo, + $review->update([ 'decision' => ReviewState::Rejected->value, 'reason' => $reason, 'notes' => null, @@ -152,10 +159,7 @@ class ShipmentReviewService } /** - * بررسی وضعیت فعلی برای عملیات Review. - * - * این check داخل transaction و بعد از lock انجام می‌شود - * تا race condition بین دو کارمند کاهش یابد. + * بررسی وضعیت فعلی Shipment برای عملیات Review. */ private function assertPendingReview( Shipment $shipment, @@ -170,7 +174,31 @@ class ShipmentReviewService } /** - * تعیین revision بعدی. + * پیدا کردن 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 { diff --git a/04_Laravel/resubmit-test.json b/04_Laravel/resubmit-test.json new file mode 100644 index 0000000..8e254ce --- /dev/null +++ b/04_Laravel/resubmit-test.json @@ -0,0 +1,36 @@ +{ + "direction": "export", + "type": "PARCEL", + "from_country_id": 96, + "to_country_id": 7, + "weight": 3.70, + "dimensions": "90*90*90", + "packages": [ + { + "weight": 3.70, + "dimensions": "90*90*90", + "description": "dfasdf" + } + ], + "sender_name": "Moshtari Updated", + "sender_phone": "989898989899", + "sender_address": "New addresssss", + "sender_city": "asdfasdf", + "receiver_name": "moshtari2", + "receiver_phone": "565656565", + "receiver_address": "sdfhsdfsdhsdgh", + "receiver_city": "dfgsdfgsdfgs", + "extra_service": 0, + "packing_cost": 0, + "domestic_pickup": 0, + "domestic_delivery": 0, + "warehousing_cost": 0, + "items": [ + { + "description": "HHHH25", + "hs_code": "989898989", + "quantity": 12, + "unit_price": 1.20 + } + ] +} \ No newline at end of file diff --git a/resubmit-test.json b/resubmit-test.json new file mode 100644 index 0000000..8e254ce --- /dev/null +++ b/resubmit-test.json @@ -0,0 +1,36 @@ +{ + "direction": "export", + "type": "PARCEL", + "from_country_id": 96, + "to_country_id": 7, + "weight": 3.70, + "dimensions": "90*90*90", + "packages": [ + { + "weight": 3.70, + "dimensions": "90*90*90", + "description": "dfasdf" + } + ], + "sender_name": "Moshtari Updated", + "sender_phone": "989898989899", + "sender_address": "New addresssss", + "sender_city": "asdfasdf", + "receiver_name": "moshtari2", + "receiver_phone": "565656565", + "receiver_address": "sdfhsdfsdhsdgh", + "receiver_city": "dfgsdfgsdfgs", + "extra_service": 0, + "packing_cost": 0, + "domestic_pickup": 0, + "domestic_delivery": 0, + "warehousing_cost": 0, + "items": [ + { + "description": "HHHH25", + "hs_code": "989898989", + "quantity": 12, + "unit_price": 1.20 + } + ] +} \ No newline at end of file