From d3de6f8997f829ff0dd5ff73b0ffb5b691ceab68 Mon Sep 17 00:00:00 2001 From: Dillon Ngo Date: Sun, 18 May 2025 21:45:32 +0800 Subject: [PATCH] E-Invoice - New business rule, transfer now has duration limit (Payment attempt duration limit) --- .../CreateBookingPaymentLogic.php | 2 + .../CheckPurchaseOrderRuleLogic.php | 2 + .../CheckTransferRuleLogic.php | 60 ++++++++++ .../CheckTransferRuleDTO.php | 25 ++++ .../Rules/CanPassOrderDurationLimitRule.php | 113 ++++++++++++++++++ .../Controllers/Rules/CheckRuleController.php | 10 ++ app/Http/Resources/V2/BookingV2Resource.php | 10 +- .../BookingPaymentQuotationV2Component.vue | 17 ++- .../forms/PurchaseOrderFormComponent.vue | 1 + routes/rule.php | 1 + 10 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 app/Classes/Modules/Rules/ControllersLogic/CheckTransferRuleLogic.php create mode 100644 app/Classes/Modules/Rules/DataTransferObjects/CheckTransferRuleDTO.php create mode 100644 app/Classes/Modules/Rules/Standards/Rules/CanPassOrderDurationLimitRule.php diff --git a/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php index ce8bec20..aa9f9670 100644 --- a/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php +++ b/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php @@ -24,6 +24,7 @@ use App\Classes\Modules\Transactions\Processors\CreateCashBackTransactionProcess use App\Classes\Modules\Vouchers\Processors\Voucherify\BookingToVoucherifyProcessor; use App\Classes\Modules\Wallets\Services\RecalculatesWalletBalance; use App\Classes\Modules\Rules\Services\RuleEvaluator; +use App\Classes\Modules\Rules\Standards\Rules\CanPassOrderDurationLimitRule; use App\Classes\ValueObjects\Constants\ApprovalStatus; use App\Classes\ValueObjects\Constants\PaymentMethodType; use App\Classes\ValueObjects\Constants\TransactionType; @@ -131,6 +132,7 @@ class CreateBookingPaymentLogic extends AbstractControllerLogic { $dto = new ConfirmBookingDTO($request->all()); $result = $this->ruleEvaluator->evaluate([ + App()->make(CanPassOrderDurationLimitRule::class), App()->make(CanPassEInvoicePromptedRule::class), App()->make(CanPassTINRule::class), App()->make(CanPassPurchaseOrderRule::class), diff --git a/app/Classes/Modules/Rules/ControllersLogic/CheckPurchaseOrderRuleLogic.php b/app/Classes/Modules/Rules/ControllersLogic/CheckPurchaseOrderRuleLogic.php index 41f24c22..a1c74aee 100644 --- a/app/Classes/Modules/Rules/ControllersLogic/CheckPurchaseOrderRuleLogic.php +++ b/app/Classes/Modules/Rules/ControllersLogic/CheckPurchaseOrderRuleLogic.php @@ -8,6 +8,7 @@ use App\Classes\General\Abstracts\AbstractControllerLogic; use App\Classes\Modules\Rules\DataTransferObjects\CheckPurchaseOrderRuleDTO; use App\Classes\Modules\Rules\Services\RuleEvaluator; use App\Classes\Modules\Rules\Standards\Rules\CanPassEInvoicePromptedRule; +use App\Classes\Modules\Rules\Standards\Rules\CanPassOrderDurationLimitRule; use App\Classes\Modules\Rules\Standards\Rules\CanPassPurchaseOrderRule; use App\Http\Resources\RuleResource; use Illuminate\Http\JsonResponse; @@ -49,6 +50,7 @@ class CheckPurchaseOrderRuleLogic extends AbstractControllerLogic $dto = new CheckPurchaseOrderRuleDTO($request->all()); $result = $this->ruleEvaluator->evaluate([ + App()->make(CanPassOrderDurationLimitRule::class), App()->make(CanPassEInvoicePromptedRule::class), App()->make(CanPassPurchaseOrderRule::class), ], $dto); diff --git a/app/Classes/Modules/Rules/ControllersLogic/CheckTransferRuleLogic.php b/app/Classes/Modules/Rules/ControllersLogic/CheckTransferRuleLogic.php new file mode 100644 index 00000000..b17d8ed0 --- /dev/null +++ b/app/Classes/Modules/Rules/ControllersLogic/CheckTransferRuleLogic.php @@ -0,0 +1,60 @@ + 'Rule Check Transfer', + 'message' => 'You have successfully passed all rules evaluated' + ]; + } + + /** @var RuleEvaluator */ + private $ruleEvaluator; + + /** + * CheckTransferRuleLogic constructor. + */ + public function __construct(RuleEvaluator $ruleEvaluator) + { + $this->ruleEvaluator = $ruleEvaluator; + } + + /** + * @param Request $request + * @return JsonResponse + * @throws \App\Classes\Exceptions\AccessForbiddenException + * @throws \App\Classes\Exceptions\MalformedRequestException + * @throws \App\Classes\Exceptions\RequestValidationException + * @throws \App\Classes\Exceptions\CriteriaNotFulfilledException + */ + public function logic(Request $request) : JsonResponse + { + $dto = new CheckTransferRuleDTO($request->all()); + + $result = $this->ruleEvaluator->evaluate([ + App()->make(CanPassOrderDurationLimitRule::class), + ], $dto); + + if ($result->failed()) { + throw new CriteriaNotFulfilledException("- " . implode("
- ", $result->messages())); + } + + return $this->resourceResponse(new RuleResource((object)$result)); + } +} diff --git a/app/Classes/Modules/Rules/DataTransferObjects/CheckTransferRuleDTO.php b/app/Classes/Modules/Rules/DataTransferObjects/CheckTransferRuleDTO.php new file mode 100644 index 00000000..35f630f2 --- /dev/null +++ b/app/Classes/Modules/Rules/DataTransferObjects/CheckTransferRuleDTO.php @@ -0,0 +1,25 @@ +bookingId = $data['booking_id']; + $this->companyId = $data['company_id']; + } + + public function toArray(): array + { + return [ + 'booking_id' => $this->bookingId, + 'company_id' => $this->companyId, + ]; + } +} diff --git a/app/Classes/Modules/Rules/Standards/Rules/CanPassOrderDurationLimitRule.php b/app/Classes/Modules/Rules/Standards/Rules/CanPassOrderDurationLimitRule.php new file mode 100644 index 00000000..46739d2b --- /dev/null +++ b/app/Classes/Modules/Rules/Standards/Rules/CanPassOrderDurationLimitRule.php @@ -0,0 +1,113 @@ +fetchesBooking = $fetchesBooking; + $this->fetchesCompanyPaymentAttemptLimit = $fetchesCompanyPaymentAttemptLimit; + } + + /** + * @return bool + */ + protected function authorized($object): bool + { + return true; + + } + + /** + * @return bool + */ + protected function validators($object): bool + { + return true; + + } + + + /** + * @return bool + */ + protected function criteria($object): bool + { + //Check if order is still valid (within duration limit, reused PAYMENT_ATTEMPT_DURATION_LIMIT) + + $isExpired = false; + $booking = $this->fetchesBooking->execute(['id' => $object->bookingId]); + $paymentAttemptLimit = $this->fetchesCompanyPaymentAttemptLimit->execute($booking->company); + // $paymentAttemptLimit = 5; //Manual testing must pay in minutes + + $createdAt = Carbon::parse($booking->created_at); + $bookingExpiresAt = $createdAt->addMinutes($paymentAttemptLimit); + $now = Carbon::now(); + + if ($now->greaterThan($bookingExpiresAt)) { + $isExpired = true; + } + + $allPayments = $booking->transactions() + ->payments() + ->get(); + + if ($isExpired) { + $filteredPayments = $allPayments->filter(function ($payment) use ($bookingExpiresAt) { + return Carbon::parse($payment->created_at)->lessThanOrEqualTo($bookingExpiresAt); + }); + + if ($filteredPayments->isNotEmpty()) { + // Use the earlier payment to recalculate bookingExpiresAt + $earliestPayment = $filteredPayments->sortBy('created_at')->first(); + $newBookingExpiresAt = Carbon::parse($earliestPayment->created_at)->addMinutes($paymentAttemptLimit); + + $logDetails = [ + 'booking_id' => $booking->id, + 'initial_created_at' => $booking->created_at, + 'original_expiry' => $bookingExpiresAt->toDateTimeString(), + 'new_expiry' => $newBookingExpiresAt->toDateTimeString(), + 'valid_payments' => [] + ]; + foreach ($filteredPayments as $payment) { + $logDetails['valid_payments'][] = [ + 'payment_id' => $payment->id, + 'created_at' => $payment->created_at, + 'amount' => $payment->amount, + ]; + } + Log::info("Booking initially expired, but found valid pending payment(s).", $logDetails); + + $bookingExpiresAt = $newBookingExpiresAt; + $isExpired = Carbon::now()->greaterThan($bookingExpiresAt); + } else { + Log::info("Booking expired and no valid pending payments for booking ID: {$booking->id}"); + } + } + + if($isExpired){ + throw new CriteriaNotFulfilledException("Transfer has already expired."); + } + return true; + } +} diff --git a/app/Http/Controllers/Rules/CheckRuleController.php b/app/Http/Controllers/Rules/CheckRuleController.php index 881c6abe..59b24cd0 100644 --- a/app/Http/Controllers/Rules/CheckRuleController.php +++ b/app/Http/Controllers/Rules/CheckRuleController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Rules; use App\Classes\Modules\Rules\ControllersLogic\CheckEInvoiceRuleLogic; use App\Classes\Modules\Rules\ControllersLogic\CheckPurchaseOrderRuleLogic; +use App\Classes\Modules\Rules\ControllersLogic\CheckTransferRuleLogic; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -26,4 +27,13 @@ class CheckRuleController public function checkPurchaseOrderRule(Request $request, CheckPurchaseOrderRuleLogic $logic): JsonResponse { return $logic->execute($request); } + + /** + * @param Request $request + * @param CheckTransferRuleLogic $logic + * @return JsonResponse + */ + public function checkTransferRule(Request $request, CheckTransferRuleLogic $logic): JsonResponse { + return $logic->execute($request); + } } diff --git a/app/Http/Resources/V2/BookingV2Resource.php b/app/Http/Resources/V2/BookingV2Resource.php index a41d7e35..e1735081 100644 --- a/app/Http/Resources/V2/BookingV2Resource.php +++ b/app/Http/Resources/V2/BookingV2Resource.php @@ -53,15 +53,19 @@ class BookingV2Resource extends JsonResource 'created_at' => Carbon::parse($this->created_at)->format('d-m-Y'), 'created_at_with_time' => Carbon::parse($this->created_at)->format('d-m-Y h:i:s A'), $this->mergeWhen($this->relationLoaded('transactions'), [ - 'purchase_order' => new V2\TransactionV2Resource($this->transactions()->where('type', TransactionType::PURCHASE_ORDER)->first()), + 'purchase_order' => new V2\TransactionV2Resource( + $this->transactions()->where('type', TransactionType::PURCHASE_ORDER)->first()), 'payment_attempts' => V2\TransactionV2Resource::collection( $this->transactions() ->payments()->where('status', ApprovalStatus::PENDING_SUBMISSION) ->whereDate('expires_on', '>=', Carbon::now()) ->get() ), - 'expired_payment_attempts' => V2\TransactionV2Resource::collection($this->transactions()->payments()->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString())->get()), - 'payment_history' => V2\TransactionV2Resource::collection($this->transactions()->where(function($query){ + 'expired_payment_attempts' => V2\TransactionV2Resource::collection( + $this->transactions()->payments()->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString())->get() + ), + 'payment_history' => V2\TransactionV2Resource::collection( + $this->transactions()->where(function($query){ $query->where(function($query){ $query->payments()->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::COMPLETED, ApprovalStatus::REJECTED]); })->orWhere(function($query){ diff --git a/resources/assets/vue/components/bookings/forms/BookingPaymentQuotationV2Component.vue b/resources/assets/vue/components/bookings/forms/BookingPaymentQuotationV2Component.vue index b5858ee3..f9267240 100644 --- a/resources/assets/vue/components/bookings/forms/BookingPaymentQuotationV2Component.vue +++ b/resources/assets/vue/components/bookings/forms/BookingPaymentQuotationV2Component.vue @@ -708,7 +708,10 @@ this.calculation = null; }, successHandler(response, section){ //E-Invoice - if(section === this.section + 'CheckEInvoiceRule'){ + if(section === this.section + 'CheckTransferRule'){ + this.checkEInvoiceRule(); + } + else if(section === this.section + 'CheckEInvoiceRule'){ if(response.payload.data.isPassed){ this.amount = (Math.round((this.data.outstanding_amount + Number.EPSILON) * 100) / 100).toFixed(2); this.expandPayment = true; @@ -751,7 +754,8 @@ $('#modal-einvoice-request').modal('show'); } else{ - this.checkEInvoiceRule(); + this.checkTransferRule(); + // this.checkEInvoiceRule(); } }, cancelQuotation(){ @@ -799,6 +803,14 @@ } }, //E-Invoice - Starts + checkTransferRule(){ + this.error = ''; + this.parameters = { + booking_id: this.data.id, + company_id: this.data.company.id, + }; + this.submit(route('api.rule.check.transfer'), 'post', this.section + 'CheckTransferRule', false, true); + }, checkEInvoiceRule(){ this.error = ''; this.parameters = { @@ -807,7 +819,6 @@ this.submit(route('api.rule.check.einvoice'), 'post', this.section + 'CheckEInvoiceRule', false, true); }, checkPurchaseOrderRule(){ - this.error = ''; this.error = ''; this.parameters = { booking_id: this.data.id, diff --git a/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue b/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue index 4636db2a..942a30e4 100644 --- a/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue +++ b/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue @@ -194,6 +194,7 @@ + diff --git a/routes/rule.php b/routes/rule.php index b2ea057b..d5330176 100644 --- a/routes/rule.php +++ b/routes/rule.php @@ -8,4 +8,5 @@ Route::prefix('rule') ->group(function () { Route::post('/check/eInvoice', [CheckRuleController::class, 'checkEInvoiceRule'])->name('check.einvoice'); Route::post('/check/purchase-order', [CheckRuleController::class, 'checkPurchaseOrderRule'])->name('check.purchase.order'); + Route::post('/check/tranfer', [CheckRuleController::class, 'checkTransferRule'])->name('check.transfer'); });