E-Invoice - New business rule, transfer now has duration limit (Payment attempt duration limit)

This commit is contained in:
Dillon Ngo
2025-05-18 21:45:32 +08:00
parent f524bc37de
commit d3de6f8997
10 changed files with 235 additions and 6 deletions
@@ -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),
@@ -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);
@@ -0,0 +1,60 @@
<?php
namespace App\Classes\Modules\Rules\ControllersLogic;
use App\Classes\Exceptions\CriteriaNotFulfilledException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Rules\DataTransferObjects\CheckTransferRuleDTO;
use App\Classes\Modules\Rules\Services\RuleEvaluator;
use App\Classes\Modules\Rules\Standards\Rules\CanPassOrderDurationLimitRule;
use App\Http\Resources\RuleResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CheckTransferRuleLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => '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("<br>- ", $result->messages()));
}
return $this->resourceResponse(new RuleResource((object)$result));
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Classes\Modules\Rules\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class CheckTransferRuleDTO implements DataTransferObject
{
public int $bookingId;
public int $companyId;
public function __construct(array $data)
{
$this->bookingId = $data['booking_id'];
$this->companyId = $data['company_id'];
}
public function toArray(): array
{
return [
'booking_id' => $this->bookingId,
'company_id' => $this->companyId,
];
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Classes\Modules\Rules\Standards\Rules;
use App\Classes\Exceptions\CriteriaNotFulfilledException;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Companies\Services\FetchesCompanyPaymentAttemptLimit;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class CanPassOrderDurationLimitRule extends AbstractRule
{
/** @var FetchesBooking */
private $fetchesBooking;
/** @var FetchesCompanyPaymentAttemptLimit */
private $fetchesCompanyPaymentAttemptLimit;
/**
* CanPassOrderDurationLimitRule constructor.
* @param FetchesBooking $fetchesBooking
* @param FetchesCompanyPaymentAttemptLimit $fetchesCompanyPaymentAttemptLimit
*/
public function __construct(FetchesBooking $fetchesBooking, FetchesCompanyPaymentAttemptLimit $fetchesCompanyPaymentAttemptLimit)
{
$this->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;
}
}
@@ -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);
}
}
+7 -3
View File
@@ -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){
@@ -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,
@@ -194,6 +194,7 @@
</div>
</div>
<button class="btn btn-xs all-caps b-rad-none btn-default bg-master-lightest w-100" @click="submitted = false" v-if="(Math.round((data.paid_amount + Number.EPSILON) * 100) / 100) === 0">Edit Purchase Order</button>
<!-- <button class="btn btn-xs all-caps b-rad-none btn-default bg-master-lightest w-100" @click="submitted = false" >Edit Purchase Order</button> -->
<button class="btn btn-xs all-caps b-rad-none btn-complete w-100 m-t-5" v-if="!data.documents.proforma_invoice && data.outstanding_amount != 0" @click="submit(route('api.booking.proforma.create', data.id), 'post', section, true, true)">Generate Proforma Invoice</button>
</div>
</div>
+1
View File
@@ -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');
});