Merge branch 'vapor/production' into dillon/122-1688-po-automation-project

This commit is contained in:
Dillon Ngo
2026-01-12 13:00:49 +08:00
39 changed files with 945 additions and 184 deletions
@@ -0,0 +1,42 @@
<?php
namespace App\Classes\Jobs\Commands\V2;
use App\Classes\Modules\Transactions\Processors\CreateInvoiceTransactionV2Processor;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CreateInvoiceTransactionV2CommandJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var object */
private $booking;
/**
* CreateInvoiceTransactionV2CommandJob constructor.
* @param object $booking
*/
public function __construct($booking)
{
$this->booking = $booking;
}
public function handle()
{
Log::info(Carbon::now() . ': Start job - Creating invoice for single booking.');
$start = new Carbon();
(App()->make(CreateInvoiceTransactionV2Processor::class))->execute($this->booking);
$end = new Carbon();
$elapsedTime = $start->diff($end)->format('%H:%I:%S');
Log::info(Carbon::now() . ': End job - Creating invoice for single booking. ElapsedTime: ' . $elapsedTime . '.');
}
}
@@ -2,31 +2,32 @@
namespace App\Classes\Modules\Billplzs\ControllersLogic;
use App\Classes\Modules\Wallets\DataTransferObjects\WalletObject;
use App\Classes\Modules\Wallets\Services\UpdatesWallet;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ResourceNotFoundException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Jobs\Commands\V2\CreateInvoiceTransactionV2CommandJob;
use App\Classes\Modules\Billplzs\DataTransferObjects\BillplzXSignatureObject;
use App\Classes\Modules\Billplzs\Services\GetBillplzBill;
use App\Classes\Modules\Transactions\Processors\CreateCashBackTransactionProcessor;
use App\Classes\Modules\Transactions\Processors\CreateReceiptVoucherTransactionProcessor;
use App\Classes\Exceptions\ResourceNotFoundException;
use App\Classes\Modules\Transactions\Services\FetchesTransaction;
use App\Classes\Modules\Transactions\Services\UpdatesTransactionStatus;
use App\Classes\Modules\Wallets\DataTransferObjects\WalletObject;
use App\Classes\Modules\Wallets\Services\RecalculatesWalletBalance;
use App\Classes\Modules\Wallets\Services\UpdatesWallet;
use App\Classes\Modules\Wallets\Services\UpdatesWalletBalance;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Http\Resources\TransactionResource;
use App\Models\Booking;
use App\Models\User;
use App\Models\Wallet;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\Modules\Billplzs\Services\GetBillplzBill;
use App\Classes\Modules\Billplzs\DataTransferObjects\BillplzXSignatureObject;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Transactions\Services\FetchesTransaction;
use App\Classes\Modules\Transactions\Services\UpdatesTransactionStatus;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use App\Classes\Modules\Wallets\Services\RecalculatesWalletBalance;
class CallbackBillplzLogic
{
@@ -117,6 +118,10 @@ class CallbackBillplzLogic
$this->createReceiptVoucherTransactionProcessor->execute($booking, $transaction);
}
if($booking){
CreateInvoiceTransactionV2CommandJob::dispatch($booking);
}
// if ($transaction->type == TransactionType::PAYMENT) {
// $cash_back_transaction = $this->createCashBackTransactionProcessor->execute($transaction);
// }
@@ -67,7 +67,13 @@ class ApprovePurchaseOrderLogic extends AbstractControllerLogic
$this->updatesTransactionStatus->execute($purchaseOrder, ApprovalStatus::APPROVED);
$this->createInvoiceTransactionProcessor->execute($booking);
$this->createInvoiceTransactionProcessor->execute($booking, "", null, [
'generateEInvoice' => false,
'generateEInvoiceWithNormalInvoiceTemplate' => false,
'generateEInvoiceRefund' => false,
'bookingOriginalStatus' => $booking->status
]
);
return $this->response([]);
}
@@ -5,6 +5,7 @@ namespace App\Classes\Modules\Bookings\ControllersLogic;
use App\Classes\Exceptions\CriteriaNotFulfilledException;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Jobs\Commands\V2\CreateInvoiceTransactionV2CommandJob;
use App\Classes\Modules\Bookings\Services\CalculatesBookingOutstanding;
use App\Classes\Modules\Bookings\Services\FetchesBookingQuotation;
use App\Classes\Modules\Companies\Services\FetchesCompanyPaymentAttemptLimit;
@@ -238,6 +239,10 @@ class CreateBookingPaymentLogic extends AbstractControllerLogic
$this->createReceiptVoucherTransactionProcessor->execute($booking, $transaction);
}
if($booking){
CreateInvoiceTransactionV2CommandJob::dispatch($booking);
}
return $this->resourceResponse(new TransactionResource($transaction));
}
@@ -2,25 +2,24 @@
namespace App\Classes\Modules\Bookings\ControllersLogic;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Jobs\Commands\V2\CreateInvoiceTransactionV2CommandJob;
use App\Classes\Modules\Bookings\DataTransferObjects\BookingObject;
use App\Classes\Modules\Bookings\DataTransferObjects\UpdateBookingInvoiceStatusObject;
use App\Classes\Modules\Bookings\Services\CalculatesBookingOutstanding;
use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Bookings\Services\UpdatesBookingInvoiceStatus;
use App\Classes\Modules\Bookings\Services\UpdatesBookingFixedAmount;
use App\Classes\Modules\Bookings\Standards\Rules\CanUpdateBooking;
use App\Classes\Modules\Transactions\Processors\CreateInvoiceTransactionV2Processor;
use App\Classes\Modules\Transactions\Services\UpdatesTransactionStatus;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Http\Resources\BookingResource;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Bookings\Standards\Rules\CanUpdateBooking;
use App\Classes\Modules\Bookings\DataTransferObjects\BookingObject;
use App\Classes\Modules\Bookings\Services\CalculatesBookingOutstanding;
use ErrorException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Classes\Exceptions\MalformedRequestException;
use Illuminate\Support\Facades\Log;
class UpdateBookingAmountLogic extends AbstractControllerLogic
@@ -52,6 +51,12 @@ class UpdateBookingAmountLogic extends AbstractControllerLogic
/** @var UpdatesTransactionStatus */
private $updatesTransactionStatus;
/** @var UpdatesBookingInvoiceStatus */
private $updatesBookingInvoiceStatus;
/** @var CreateInvoiceTransactionV2Processor */
private $createInvoiceTransactionProcessor;
/**
* UpdateBookingAmountLogic constructor.
* @param CanUpdateBooking $canUpdateBooking
@@ -59,14 +64,18 @@ class UpdateBookingAmountLogic extends AbstractControllerLogic
* @param FetchesBooking $fetchesBooking
* @param CalculatesBookingOutstanding $calculatesBookingOutstanding
* @param UpdatesTransactionStatus $updatesTransactionStatus
* @param UpdatesBookingInvoiceStatus $updatesBookingInvoiceStatus
* @param CreateInvoiceTransactionV2Processor $createInvoiceTransactionProcessor
*/
public function __construct(CanUpdateBooking $canUpdateBooking, UpdatesBookingFixedAmount $updatesBookingFixedAmount, FetchesBooking $fetchesBooking, CalculatesBookingOutstanding $calculatesBookingOutstanding, UpdatesTransactionStatus $updatesTransactionStatus)
public function __construct(CanUpdateBooking $canUpdateBooking, UpdatesBookingFixedAmount $updatesBookingFixedAmount, FetchesBooking $fetchesBooking, CalculatesBookingOutstanding $calculatesBookingOutstanding, UpdatesTransactionStatus $updatesTransactionStatus, UpdatesBookingInvoiceStatus $updatesBookingInvoiceStatus, CreateInvoiceTransactionV2Processor $createInvoiceTransactionProcessor)
{
$this->canUpdateBooking = $canUpdateBooking;
$this->updatesBookingFixedAmount = $updatesBookingFixedAmount;
$this->fetchesBooking = $fetchesBooking;
$this->calculatesBookingOutstanding = $calculatesBookingOutstanding;
$this->updatesTransactionStatus = $updatesTransactionStatus;
$this->updatesBookingInvoiceStatus = $updatesBookingInvoiceStatus;
$this->createInvoiceTransactionProcessor = $createInvoiceTransactionProcessor;
}
/**
@@ -81,7 +90,7 @@ class UpdateBookingAmountLogic extends AbstractControllerLogic
$input_amount = number_format( floatval(str_replace(',', '', $request->input('fix_amount', $booking->fix_amount))), 5, '.', '');
$minimum_amount = $booking->fix_amount - $this->calculatesBookingOutstanding->execute($booking);
if (((float)$input_amount + 0.01) < (float)$minimum_amount) {
$input_amount = 0;
// throw new MalformedRequestException('Booking Amount cannot be less than '. $minimum_amount .'.');
@@ -97,7 +106,15 @@ class UpdateBookingAmountLogic extends AbstractControllerLogic
$booking = $this->updatesBookingFixedAmount->execute($booking, $input_amount);
if($booking->invoice_status === ApprovalStatus::APPROVED){
$updateBookingInvoiceStatusObject = new UpdateBookingInvoiceStatusObject($booking->id, ApprovalStatus::PENDING_VERIFICATION);
$this->updatesBookingInvoiceStatus->execute($booking, $updateBookingInvoiceStatusObject);
}
else{
$this->createInvoiceTransactionProcessor->execute($booking);
}
return $this->resourceResponse(new BookingResource($booking));
}
}
}
@@ -14,6 +14,7 @@ use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Accounts\DataTransferObjects\KeyValuePairObject;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Http\Resources\BookingResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -90,9 +91,8 @@ class UpdateBookingAmountOnHoldLogic extends AbstractControllerLogic
throw new MalformedRequestException('Booking Amount cannot be less than '. $minimum_amount .'.');
}
$key = "BOOKING_AMOUNT_UPDATE";
$keyValuePairObject = new KeyValuePairObject($key, $input_amount);
$metadata = $booking->attributesKVP()->where('key', $key)->first();
$keyValuePairObject = new KeyValuePairObject(KVPKey::BOOKING_AMOUNT_UPDATE, $input_amount);
$metadata = $booking->attributesKVP()->where('key', KVPKey::BOOKING_AMOUNT_UPDATE)->first();
if($metadata){
$this->updatesKeyValuePair->execute($metadata, $keyValuePairObject);
}
@@ -10,6 +10,11 @@ use App\Classes\Modules\Bookings\Services\UpdatesBookingFixedAmount;
use App\Classes\Modules\Bookings\Services\CalculatesBookingOutstanding;
use App\Classes\Modules\Bookings\Services\CalculatesBookingPayableAmount;
use App\Classes\Modules\Bookings\Services\CalculatesBookingRefundAmount;
use App\Classes\Modules\Bookings\Services\UpdatesBookingInvoiceStatus;
use App\Classes\Modules\Transactions\Processors\CreateInvoiceTransactionV2Processor;
use App\Classes\Modules\Bookings\DataTransferObjects\UpdateBookingInvoiceStatusObject;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Http\Resources\BookingResource;
use App\Models\Booking;
@@ -45,6 +50,12 @@ class UpdateBookingAmountWithPOLogic extends AbstractControllerLogic
/** @var CalculatesBookingRefundAmount */
private $calculatesBookingRefundAmount;
/** @var UpdatesBookingInvoiceStatus */
private $updatesBookingInvoiceStatus;
/** @var CreateInvoiceTransactionV2Processor */
private $createInvoiceTransactionProcessor;
/**
* UpdateBookingAmountWithPOLogic constructor.
* @param FetchesBooking $fetchesBooking
@@ -52,14 +63,18 @@ class UpdateBookingAmountWithPOLogic extends AbstractControllerLogic
* @param CalculatesBookingOutstanding $calculatesBookingOutstanding
* @param CalculatesBookingPayableAmount $calculatesBookingPayableAmount
* @param CalculatesBookingRefundAmount $calculatesBookingRefundAmount
* @param UpdatesBookingInvoiceStatus $updatesBookingInvoiceStatus
* @param CreateInvoiceTransactionV2Processor $createInvoiceTransactionProcessor
*/
public function __construct(FetchesBooking $fetchesBooking, UpdatesBookingFixedAmount $updatesBookingFixedAmount, CalculatesBookingOutstanding $calculatesBookingOutstanding, CalculatesBookingPayableAmount $calculatesBookingPayableAmount, CalculatesBookingRefundAmount $calculatesBookingRefundAmount)
public function __construct(FetchesBooking $fetchesBooking, UpdatesBookingFixedAmount $updatesBookingFixedAmount, CalculatesBookingOutstanding $calculatesBookingOutstanding, CalculatesBookingPayableAmount $calculatesBookingPayableAmount, CalculatesBookingRefundAmount $calculatesBookingRefundAmount, UpdatesBookingInvoiceStatus $updatesBookingInvoiceStatus, CreateInvoiceTransactionV2Processor $createInvoiceTransactionProcessor)
{
$this->fetchesBooking = $fetchesBooking;
$this->updatesBookingFixedAmount = $updatesBookingFixedAmount;
$this->calculatesBookingOutstanding = $calculatesBookingOutstanding;
$this->calculatesBookingPayableAmount = $calculatesBookingPayableAmount;
$this->calculatesBookingRefundAmount = $calculatesBookingRefundAmount;
$this->updatesBookingInvoiceStatus = $updatesBookingInvoiceStatus;
$this->createInvoiceTransactionProcessor = $createInvoiceTransactionProcessor;
}
/**
@@ -74,7 +89,7 @@ class UpdateBookingAmountWithPOLogic extends AbstractControllerLogic
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $request->route('id') ?? $id]);
$outstandingAmount = $this->calculatesBookingOutstanding->execute($booking);
$bookingAttribute = $booking->attributesKVP()->where('key', "BOOKING_AMOUNT_UPDATE")->first();
$bookingAttribute = $booking->attributesKVP()->where('key', KVPKey::BOOKING_AMOUNT_UPDATE)->first();
// cief todo: 90 - BEFORE REVERT
@@ -110,6 +125,14 @@ class UpdateBookingAmountWithPOLogic extends AbstractControllerLogic
$booking->transactions()->where('type', TransactionType::PROFORMA)->delete();
$booking = $this->updatesBookingFixedAmount->execute($booking, $bookingAmountUpdate);
if($booking->invoice_status === ApprovalStatus::APPROVED){
$updateBookingInvoiceStatusObject = new UpdateBookingInvoiceStatusObject($booking->id, ApprovalStatus::PENDING_VERIFICATION);
$this->updatesBookingInvoiceStatus->execute($booking, $updateBookingInvoiceStatusObject);
}
else{
$this->createInvoiceTransactionProcessor->execute($booking);
}
$request->merge(['is_privilleged_update' => true]);
$bookingAttribute->delete();
@@ -0,0 +1,42 @@
<?php
namespace App\Classes\Modules\Bookings\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class UpdateBookingInvoiceStatusObject implements DataTransferObject
{
/** @var int */
private $id;
/** @var int */
private $invoiceStatus;
/**
* UpdateBookingInvoiceStatusObject constructor.
* @param int $id
* @param int $invoiceStatus
*/
public function __construct(int $id, int $invoiceStatus)
{
$this->id = $id;
$this->invoiceStatus = $invoiceStatus;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return int
*/
public function getInvoiceStatus(): int
{
return $this->invoiceStatus;
}
}
@@ -88,7 +88,13 @@ class CreatePurchaseOrderFor1688OrderProcessor
if($purchaseOrder->amount === $booking->fix_amount){
$this->updatesTransactionStatus->execute($purchaseOrder, ApprovalStatus::APPROVED);
$this->createInvoiceTransactionProcessor->execute($booking);
$this->createInvoiceTransactionProcessor->execute($booking, "", null, [
'generateEInvoice' => false,
'generateEInvoiceWithNormalInvoiceTemplate' => false,
'generateEInvoiceRefund' => false,
'bookingOriginalStatus' => ApprovalStatus::COMPLETED
]
);
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Classes\Modules\Bookings\Services;
use App\Classes\General\Eloquent\AbstractUpdateRecord;
use App\Classes\Modules\Bookings\DataTransferObjects\UpdateBookingInvoiceStatusObject;
use App\Models\Booking;
class UpdatesBookingInvoiceStatus extends AbstractUpdateRecord
{
/**
* @param Booking $model
* @param UpdateBookingInvoiceStatusObject $object
* @return \Illuminate\Database\Eloquent\Model
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute(Booking $model, UpdateBookingInvoiceStatusObject $object)
{
$model->invoice_status = $object->getInvoiceStatus();
return $this->handler($model);
}
}
@@ -58,6 +58,7 @@ class FetchCompanyEInvoiceInfoLogic extends AbstractControllerLogic
$eInvoiceInfo->tin = $company->tin;
$eInvoiceInfo->msic_code = $company->msic_code;
$eInvoiceInfo->e_invoice = $company->e_invoice;
$eInvoiceInfo->e_invoice_requested_at = $company->e_invoice_requested_at;
}
else{
return $this->response([]);
@@ -2,6 +2,7 @@
namespace App\Classes\Modules\Exports\Services;
use App\Classes\Jobs\Commands\V2\CreateInvoiceTransactionV2CommandJob;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Models\Booking;
@@ -71,7 +72,8 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR
'transactions.voucherRedemption',
'transactions.voucherRedemption.voucher.campaign'
])
->where('status', ApprovalStatus::COMPLETED)
// ->whereIn('status', [ApprovalStatus::COMPLETED])
->whereIn('invoice_status', [ApprovalStatus::APPROVED])
->whereHas('transactions', function ($query) use ($startDate, $endDate) {
$query->payments()
->complete()
@@ -115,6 +117,11 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR
}
$invoiceTransaction = $booking->transactions->where('type', TransactionType::INVOICE)->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->first();
if(!$invoiceTransaction){
Log::info('ExportsSalesInvoiceReport booking with NO invoice: ' . json_encode($booking));
// CreateInvoiceTransactionV2CommandJob::dispatch($booking);
return $records;
}
$currencyId = $booking->fix_currency_id;
@@ -0,0 +1,67 @@
<?php
namespace App\Classes\Modules\Rules\ControllersLogic;
use App\Classes\Exceptions\CriteriaNotFulfilledException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Rules\DataTransferObjects\CheckEInvoiceAmountLimitRuleDTO;
use App\Classes\Modules\Rules\Services\RuleEvaluator;
use App\Classes\Modules\Rules\Standards\Rules\CanPassMustEInvoiceAmountLimitRule;
use App\Http\Resources\RuleResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CheckEInvoiceAmountLimitLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => 'Rule Check E-Invoice Amount Limit',
'message' => 'You have successfully passed all rules evaluated'
];
}
/** @var RuleEvaluator */
private $ruleEvaluator;
/** @var CanPassMustEInvoiceAmountLimitRule */
private $canPassMustEInvoiceAmountLimitRule;
/**
* CheckEInvoiceAmountLimitLogic constructor.
* @param RuleEvaluator $ruleEvaluator
* @param CanPassMustEInvoiceAmountLimitRule $canPassMustEInvoiceAmountLimitRule
*/
public function __construct(RuleEvaluator $ruleEvaluator, CanPassMustEInvoiceAmountLimitRule $canPassMustEInvoiceAmountLimitRule)
{
$this->ruleEvaluator = $ruleEvaluator;
$this->canPassMustEInvoiceAmountLimitRule = $canPassMustEInvoiceAmountLimitRule;
}
/**
* @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 CheckEInvoiceAmountLimitRuleDTO($request->all());
$result = $this->ruleEvaluator->evaluate([
$this->canPassMustEInvoiceAmountLimitRule
], $dto);
if ($result->failed()) {
throw new CriteriaNotFulfilledException("- " . implode("<br>- ", $result->messages()));
}
return $this->resourceResponse(new RuleResource((object)$result));
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Classes\Modules\Rules\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class CheckEInvoiceAmountLimitRuleDTO implements DataTransferObject
{
public int $bookingId;
public int $companyId;
public ?string $amount;
public ?string $paymentMethod;
public ?string $voucherCode;
public function __construct(array $data)
{
$this->bookingId = $data['booking_id'];
$this->companyId = $data['company_id'];
$this->amount = $data['amount'] ?? null;
$this->paymentMethod = $data['payment_method'] ?? null;
$this->voucherCode = $data['voucherCode'] ?? null;
}
public function toArray(): array
{
return [
'booking_id' => $this->bookingId,
'company_id' => $this->companyId,
'amount' => $this->amount,
'payment_method' => $this->paymentMethod,
'voucher_code' => $this->voucherCode,
];
}
}
@@ -0,0 +1,102 @@
<?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\Bookings\Services\CalculatesBookingOutstanding;
use App\Classes\Modules\Bookings\Services\FetchesBookingQuotation;
use App\Classes\Modules\Currencies\DataTransferObjects\CurrencyConversionObject;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class CanPassMustEInvoiceAmountLimitRule extends AbstractRule
{
/** @var FetchesBooking */
private $fetchesBooking;
/** @var FetchesBookingQuotation */
private $fetchesBookingQuotation;
/** @var CalculatesBookingOutstanding */
private $calculatesBookingOutstanding;
/**
* CanPassMustEInvoiceAmountLimitRule constructor.
* @param FetchesBooking $fetchesBooking
* @param FetchesBookingQuotation $fetchesBookingQuotation
* @param CalculatesBookingOutstanding $calculatesBookingOutstanding
*/
public function __construct(FetchesBooking $fetchesBooking, FetchesBookingQuotation $fetchesBookingQuotation, CalculatesBookingOutstanding $calculatesBookingOutstanding)
{
$this->fetchesBooking = $fetchesBooking;
$this->fetchesBookingQuotation = $fetchesBookingQuotation;
$this->calculatesBookingOutstanding = $calculatesBookingOutstanding;
}
/**
* @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 booking amount is RM10,000 or more, than must opt-in E-Invoice
$booking = $this->fetchesBooking->execute(['id' => $object->bookingId]);
$company = $booking->company;
// $totalPayments = 0;
// if(isset($object->amount)){
// $totalPayments = $booking->transactions()->payments()->whereIn('transactions.status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->sum('amount');
// $conversionObject = new CurrencyConversionObject(floatval(str_replace(',', '', floatval(str_replace(',', '', $object->amount)))), $booking->convertible_currency_id, $booking->service_id, $booking->fix_currency_id === 1 ? 0:1, PaymentMethodType::PAYMENT_METHODS[$object->paymentMethod]);
// $voucherCode = $object->voucherCode ?? null;
// $configurations = $this->fetchesBookingQuotation->execute($booking->company, $conversionObject, $voucherCode, null, $booking);
// }
// else{
// $outstanding = $this->calculatesBookingOutstanding->execute($booking);
// $conversionObject = new CurrencyConversionObject(floatval(str_replace(',', '', $outstanding)), $booking->convertible_currency_id, $booking->service_id, $booking->fix_currency_id === 1 ? 0 : 1, PaymentMethodType::CASH);
// $configurations = $this->fetchesBookingQuotation->execute($booking->company, $conversionObject);
// }
// $bookingAmountInMYR = $configurations->getTotal();
// $bookingAmountInMYR = $bookingAmountInMYR + $totalPayments;
// if($bookingAmountInMYR >= 10000 && (!$company->e_invoice || !$company->tin) ){
// throw new CriteriaNotFulfilledException("RM10,000 and above must opt-in for E-Invoice.");
// }
//1 MYR, 2 CNY, 3 USD
if((($booking->fix_amount >= 9000 && $booking->fix_currency_id === 1) ||
($booking->fix_amount >= 12600 && $booking->fix_currency_id === 2) ||
($booking->fix_amount >= 1920 && $booking->fix_currency_id === 3))
&& (!$company->e_invoice || !$company->tin) ){
throw new CriteriaNotFulfilledException("Booking amount above limit, must opt-in for E-Invoice.");
}
return true;
}
}
@@ -90,7 +90,13 @@ class CreatePaymentProofDocumentLogic extends AbstractControllerLogic
$this->updatesTransactionStatus->execute($transaction, ApprovalStatus::APPROVED);
$this->createInvoiceTransactionProcessor->execute($transaction->owner->booking);
$this->createInvoiceTransactionProcessor->execute($transaction->owner->booking, "", null, [
'generateEInvoice' => false,
'generateEInvoiceWithNormalInvoiceTemplate' => false,
'generateEInvoiceRefund' => false,
'bookingOriginalStatus' => ApprovalStatus::COMPLETED
]
);
// send email to customer
// todo: a function to send a proof to the receipiant, they have to give us a email of the receipiant and also need to submiited purchase order
@@ -104,14 +104,20 @@ class CreateInvoiceTransactionV2Processor
$generateEInvoice = $options['generateEInvoice'] ?? false;
$generateEInvoiceWithNormalInvoiceTemplate = $options['generateEInvoiceWithNormalInvoiceTemplate'] ?? false;
$generateEInvoiceRefund = $options['generateEInvoiceRefund'] ?? false;
$bookingOriginalStatus = $options['bookingOriginalStatus'] ?? ApprovalStatus::COMPLETED;
$bookingOriginalStatus = $options['bookingOriginalStatus'] ?? ApprovalStatus::APPROVED;
// Log::info('CreateInvoiceTransactionV2Processor generateEInvoice:' . json_encode($generateEInvoice));
// Log::info('CreateInvoiceTransactionV2Processor generateEInvoiceWithNormalInvoiceTemplate: ' . json_encode($generateEInvoiceWithNormalInvoiceTemplate));
// Log::info('CreateInvoiceTransactionV2Processor generateEInvoiceRefund: ' . json_encode($generateEInvoiceRefund));
// Log::info('CreateInvoiceTransactionV2Processor bookingOriginalStatus: ' . json_encode($bookingOriginalStatus));
if($generateEInvoiceRefund){
$generateEInvoice = true; //With or without refund, these 2 flags are meant to Generate E-Invoice, so cannot have opposite indicator
}
if ($booking->status === ApprovalStatus::COMPLETED && !$generateEInvoice) {
return;
if ($booking->status === ApprovalStatus::COMPLETED && !$generateEInvoiceRefund) {
Log::info('CreateInvoiceTransactionV2Processor Check 1 Bypass New Business Logic Update for booking ' . $booking->id);
// return;
}
$payable_amount = $this->calculatesBookingPayableAmount->execute($booking, $booking->fix_currency_id, $generateEInvoiceRefund);
@@ -128,31 +134,27 @@ class CreateInvoiceTransactionV2Processor
}
// confirm that all payments has been transferred
if ($this->calculatesBookingTransferredAmount->execute($booking) !== $this->calculatesBookingPaidAmount->execute($booking)) {
return;
Log::info('CreateInvoiceTransactionV2Processor Check 2 Bypass New Business Logic Update for booking ' . $booking->id);
// return;
}
if($generateEInvoiceRefund){
$purchaseOrder = $booking->transactions()
->where('type', TransactionType::PURCHASE_ORDER)
->complete()
->first();
if(!$purchaseOrder){
//was use for $generateEInvoiceRefund true
$purchaseOrder = $booking->transactions()
->where('type', TransactionType::PURCHASE_ORDER)
->where('status', ApprovalStatus::PENDING_SUBMISSION)
->first();
}
else{
$purchaseOrder = $booking->transactions()
->where('type', TransactionType::PURCHASE_ORDER)
->complete()
->first();
}
$constants = SegmentConstant::where('reference', SegmentConstants::SERVICE_TYPE)->where('detail->id', $booking->service->id)->first();
if ($constants->detail->is_billable && !$purchaseOrder && !$generateEInvoiceRefund) {
return;
}
// $transaction = $booking->transactions()
// ->where('type', TransactionType::PAYMENT)
// ->first();
// if ($constants->detail->is_billable && !$purchaseOrder && !$generateEInvoiceRefund) {
// return;
// }
$transaction = $booking->transactions()
->where('type', TransactionType::PAYMENT)
@@ -163,7 +165,8 @@ class CreateInvoiceTransactionV2Processor
$eInvoice = false;
$eInvoiceStartDate = Carbon::parse(env('E_INVOICE_START_DATE', '2025-07-01 00:00:00'));
$bookingCreatedDate = Carbon::parse($booking->created_at);
if ($bookingCreatedDate->isAfter($eInvoiceStartDate) && $supplier->e_invoice === 1) {
$eInvoiceRequestedDate = Carbon::parse($supplier->e_invoice_requested_at);
if ($bookingCreatedDate->isAfter($eInvoiceStartDate) && $bookingCreatedDate->isAfter($eInvoiceRequestedDate) && $supplier->e_invoice === 1) {
$eInvoice = true;
}
$kvp = $booking->attributesKVP()->where('key', KVPKey::BOOKING_EINVOICE_ELIGIBLE)->first();
@@ -211,84 +214,20 @@ class CreateInvoiceTransactionV2Processor
->sum('tax');
}
$transaction_object = new TransactionObject(
$billNumber,
TransactionType::INVOICE,
$transaction->issuer,
$transaction->receiver,
$transaction->recipient_bank_account_id,
$transaction->payment_method,
$payable_amount,
$booking_amount,
$transaction->currency_id,
$transaction->original_currency_id,
$booking_currency_average_rate,
$total_tax,
$total_service_charge,
null,
ApprovalStatus::APPROVED
);
$invoice_transaction = $this->createsTransaction->execute($purchaseOrder->booking ?? $booking, $transaction_object);
if ($kvpCopies) {
foreach ($kvpCopies as $kvp) {
$invoice_transaction->attributesKVP()->save($kvp);
}
$invoice_transaction = null;
if($booking->status === ApprovalStatus::APPROVED){
//NEW INVOICE for non-einvoice user applicable only when booking is completed
$invoice_transaction = $booking->transactions()
->where('type', TransactionType::INVOICE)
->complete()
->latest()
->first();
}
$voucherRedemption = $transaction->voucherRedemption;
// purchase order
if(!$generateEInvoiceRefund){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::PURCHASE_ORDER, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate);
}
// deliver order
if(!$generateEInvoiceRefund){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::DELIVER_ORDER, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate);
}
// e-invoice
if ($eInvoice)
{
if($generateEInvoice){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::EINVOICE, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate, $generateEInvoiceRefund);
}
}
// invoice
else
{
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::INVOICE, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate, $generateEInvoiceRefund);
}
if(!$generateEInvoiceRefund){
$billNumber = $this->generatesTransactionBillNumber->execute('SPDO-');
$booking_currency_average_rate = $this->calculatesBookingCurrencyAverageRate->execute($booking, TransactionType::BILL, $generateEInvoiceRefund);
$paymentTransaction = $booking->transactions()->payments()->where('status', ApprovalStatus::COMPLETED)->first();
$transaction = null;
if($paymentTransaction){
$transaction = $paymentTransaction->transactions()->where('type', TransactionType::BILL)->first();
}
else{ // Special handling for refund cases (When a refund is deleted via DeleteRefundTransactionLogic, a booking payment transaction is set to ApprovalStatus::APPROVED)
$temp = $booking->transactions()->payments()->where('status', ApprovalStatus::APPROVED)->first();
// Lets check if there is a refund case
$refund = $temp->transactions()->refunds()->where('status', ApprovalStatus::APPROVED)->first();
if($refund){
$transaction = $temp;
}
else{
throw new Exception("No payment found for booking '$booking->id'.");
}
}
if(!$invoice_transaction) {
$transaction_object = new TransactionObject(
$billNumber,
TransactionType::SUPPLIER_DELIVER,
TransactionType::INVOICE,
$transaction->issuer,
$transaction->receiver,
$transaction->recipient_bank_account_id,
@@ -303,19 +242,101 @@ class CreateInvoiceTransactionV2Processor
null,
ApprovalStatus::APPROVED
);
$supplier_deliver_order_transaction = $this->createsTransaction->execute($purchaseOrder->booking, $transaction_object);
$invoice_transaction = $this->createsTransaction->execute($purchaseOrder->booking ?? $booking, $transaction_object);
}
// supply deliver order
$this->invoiceDocumentProcessor->execute($supplier_deliver_order_transaction, $purchaseOrder, $supplier, DocumentType::SUPPLIER_DELIVER_ORDER, null);
//Update booking table, used on Export Sales Invoice Report
$booking->invoice_status = ApprovalStatus::APPROVED;
$booking->save();
if ($kvpCopies) {
foreach ($kvpCopies as $kvp) {
$invoice_transaction->attributesKVP()->save($kvp);
}
}
$voucherRedemption = $transaction->voucherRedemption;
if($purchaseOrder){
// purchase order
if(!$generateEInvoiceRefund){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::PURCHASE_ORDER, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate);
}
// deliver order
if(!$generateEInvoiceRefund){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::DELIVER_ORDER, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate);
}
// e-invoice
if ($eInvoice)
{
if($generateEInvoice){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::EINVOICE, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate, $generateEInvoiceRefund);
}
}
// invoice
else
{
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::INVOICE, $voucherRedemption, $generateEInvoiceWithNormalInvoiceTemplate, $generateEInvoiceRefund);
}
if(!$generateEInvoiceRefund){
$billNumber = $this->generatesTransactionBillNumber->execute('SPDO-');
$booking_currency_average_rate = $this->calculatesBookingCurrencyAverageRate->execute($booking, TransactionType::BILL, $generateEInvoiceRefund);
$paymentTransaction = $booking->transactions()->payments()->where('status', ApprovalStatus::COMPLETED)->first();
$transactionTypeBill= null;
if($paymentTransaction){
$transactionTypeBill = $paymentTransaction->transactions()->where('type', TransactionType::BILL)->first();
}
else{ // Special handling for refund cases (When a refund is deleted via DeleteRefundTransactionLogic, a booking payment transaction is set to ApprovalStatus::APPROVED)
$paymentTransactionTemp = $booking->transactions()->payments()->where('status', ApprovalStatus::APPROVED)->first();
// Lets check if there is a refund case
$refund = $paymentTransactionTemp->transactions()->refunds()->where('status', ApprovalStatus::APPROVED)->first();
if($refund){
$transactionTypeBill = $paymentTransactionTemp->transactions()->where('type', TransactionType::BILL)->first();
}
else{
Log::info("CreateInvoiceTransactionV2Processor NO completed payment transaction nor refund transaction found for booking '$booking->id'.");
}
}
if($transactionTypeBill){
$transaction_object = new TransactionObject(
$billNumber,
TransactionType::SUPPLIER_DELIVER,
$transactionTypeBill->issuer,
$transactionTypeBill->receiver,
$transactionTypeBill->recipient_bank_account_id,
$transactionTypeBill->payment_method,
$payable_amount,
$booking_amount,
$transactionTypeBill->currency_id,
$transactionTypeBill->original_currency_id,
$booking_currency_average_rate,
$total_tax,
$total_service_charge,
null,
ApprovalStatus::APPROVED
);
$supplier_deliver_order_transaction = $this->createsTransaction->execute($purchaseOrder->booking, $transaction_object);
// supply deliver order
$this->invoiceDocumentProcessor->execute($supplier_deliver_order_transaction, $purchaseOrder, $supplier, DocumentType::SUPPLIER_DELIVER_ORDER, null);
}
}
// update perfex crm
// if(config('perfexcrm.is_enabled') == 'true'){
// CreatePerfexCRMInvoice::dispatch($invoice_transaction, $purchaseOrder, $supplier);
// }
}
//if(!$generateEInvoiceRefund){
$this->updatesBookingStatus->execute($booking, $bookingOriginalStatus);
//}
// update perfex crm
// if(config('perfexcrm.is_enabled') == 'true'){
// CreatePerfexCRMInvoice::dispatch($invoice_transaction, $purchaseOrder, $supplier);
// }
}
}
@@ -86,6 +86,7 @@ class CreateSupplierTransactionProcessor
$original_amount_after_refund = $payment->original_amount - $totalRefund;
$this->updatesTransactionStatus->execute($payment, ApprovalStatus::COMPLETED);
$billNumber = $this->generatesTransactionBillNumber->execute('SPLR-');
$constant = SegmentConstant::where('reference', SegmentConstants::SERVICE_CHARGE)->where('detail->id', $supplier->id)->first();
@@ -29,5 +29,5 @@ final class ApprovalStatus {
self::SUSPENDED => "Suspended",
self::EXPIRED => "Expired",
self::REFUNDED => "Refunded",
];
];
}
@@ -30,4 +30,7 @@ class KVPKey
public const CHATGPT_PROMPT_PREFIX = 'CHATGPT_PROMPT_';
public const CLAUDE_PROMPT_PREFIX = 'CLAUDE_PROMPT_';
public const BOOKING_AMOUNT_UPDATE = 'BOOKING_AMOUNT_UPDATE';
}
@@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands\V2;
use App\Classes\Jobs\Commands\V2\OneTimeBatchProcessEInvoicesV2CommandJob;
use App\Classes\Jobs\Commands\V2\ProcessBookingForEInvoiceV2CommandJob;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\DocumentType;
use App\Classes\ValueObjects\Constants\TransactionType;
use Illuminate\Console\Command;
use App\Models\Booking;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
class OneTimeProcessBookingForEInvoiceV2Command extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'one-time-process-booking-for-einvoice-command';
/**
* The console command description.
*
* @var string
*/
protected $description = 'One time batch process E-Invoices';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$bookings = Booking::whereIn('marking', ['821671', '773802','106426'])->get();
$count = 0;
foreach ($bookings as $booking) {
ProcessBookingForEInvoiceV2CommandJob::dispatch($booking);
$count++;
Log::info('Processed: ' . $count);
Log::info('Booking ID: ' . $booking->marking . ' for E-Invoice ');
}
}
}
@@ -5,6 +5,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 App\Classes\Modules\Rules\ControllersLogic\CheckEInvoiceAmountLimitLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -36,4 +37,13 @@ class CheckRuleController
public function checkTransferRule(Request $request, CheckTransferRuleLogic $logic): JsonResponse {
return $logic->execute($request);
}
/**
* @param Request $request
* @param CheckEInvoiceAmountLimitLogic $logic
* @return JsonResponse
*/
public function checkEInvoiceAmountLimitRule(Request $request, CheckEInvoiceAmountLimitLogic $logic): JsonResponse {
return $logic->execute($request);
}
}
+9 -9
View File
@@ -13,6 +13,7 @@ use App\Classes\ValueObjects\Constants\DocumentType;
use App\Classes\ValueObjects\Constants\KVPKey;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Log;
class BookingResource extends JsonResource
{
@@ -30,7 +31,7 @@ class BookingResource extends JsonResource
$bookingCreatedDate = Carbon::parse($this->created_at);
$eInvoiceRequestedDate = Carbon::parse($this->company->e_invoice_requested_at);
//cief todo: 90 - for testing
if ($bookingCreatedDate->isAfter($eInvoiceStartDate) && $this->company->e_invoice === 1) { //&& $bookingCreatedDate->diffInMinutes($eInvoiceRequestedDate) <= 480 cief todo: 90
if ($bookingCreatedDate->isAfter($eInvoiceStartDate) && $bookingCreatedDate->isAfter($eInvoiceRequestedDate) && $this->company->e_invoice === 1) { //&& $bookingCreatedDate->diffInMinutes($eInvoiceRequestedDate) <= 480 cief todo: 90
$eInvoice = true;
}
@@ -58,15 +59,13 @@ class BookingResource extends JsonResource
'convertible_currency' => new CurrencyResource($this->convertibleCurrency),
'conversion_currency' => new CurrencyResource($this->conversionCurrency),
'documents' => [
'purchase_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::PURCHASE_ORDER)->first()),
'delivery_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::DELIVER_ORDER)->first()),
'invoice' => new DocumentResource($this->documents()->where('document_type', DocumentType::INVOICE)->first()),
'purchase_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::PURCHASE_ORDER)->latest()->first()),
'delivery_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::DELIVER_ORDER)->latest()->first()),
'invoice' => new DocumentResource($this->documents()->where('document_type', DocumentType::INVOICE)->latest()->first()),
'e_invoice' => new DocumentResource($this->documents()->where('document_type', DocumentType::EINVOICE)->latest()->first()),
'supplier_delivery_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::SUPPLIER_DELIVER_ORDER)->first()),
'proforma_invoice' => new DocumentResource($this->documents()->where('document_type', DocumentType::PROFORMA_INVOICE)->whereNotIn('status', [ApprovalStatus::REJECTED, ApprovalStatus::EXPIRED])->orderByDesc('id')->first()),
'ecommerce_purchase_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->first()),
'banking_invoice' => new DocumentResource($this->documents()->where('document_type', DocumentType::BANKING_INVOICE)->whereNotIn('status', [ApprovalStatus::REJECTED, ApprovalStatus::EXPIRED])->orderByDesc('id')->first()),
'delivery_order_banking' => new DocumentResource($this->documents()->where('document_type', DocumentType::DELIVER_ORDER_BANKING)->orderByDesc('id')->first()),
'supplier_delivery_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::SUPPLIER_DELIVER_ORDER)->latest()->first()),
'proforma_invoice' => new DocumentResource($this->documents()->where('document_type', DocumentType::PROFORMA_INVOICE)->whereNotIn('status', [ApprovalStatus::REJECTED, ApprovalStatus::EXPIRED])->latest()->first()),
'ecommerce_purchase_order' => new DocumentResource($this->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->latest()->first()),
],
'order_reference_no' => $this->modelAttributes()->where('name', BookingAttributeNames::ORDER_REFERENCE_NO)->get()->map(function ($attr) {
return [
@@ -100,6 +99,7 @@ class BookingResource extends JsonResource
})->latest()->get())
]),
'einvoice' => $eInvoice,
'invoice_status' => $this->invoice_status,
];
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Resources;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
class EInvoiceInfoResource extends JsonResource
@@ -26,6 +27,7 @@ class EInvoiceInfoResource extends JsonResource
'msic_code' => (string) $this->msic_code,
'tin' => (string) $this->tin,
'e_invoice' => (int) $this->e_invoice,
'e_invoice_requested_date' => $this->e_invoice_requested_at ? Carbon::parse($this->e_invoice_requested_at)->format('d-m-Y') : null,
];
}
}
@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddInvoiceStatusToBookingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('bookings', function (Blueprint $table) {
$table->unsignedTinyInteger('invoice_status')
->default(0)
->after('status');
$table->index('invoice_status', 'bookings_invoice_status_index');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropIndex('bookings_invoice_status_index');
$table->dropColumn('invoice_status');
});
}
}
@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIsInvoiceGeneratedToBookingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('bookings', function (Blueprint $table) {
$table->boolean('is_invoice_generated')
->default(false)
->after('status');
$table->index('is_invoice_generated', 'bookings_is_invoice_generated_index');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('bookings', function (Blueprint $table) {
$table->dropIndex('bookings_is_invoice_generated_index');
$table->dropColumn('is_invoice_generated');
});
}
}
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddInvoiceStatusToBookingLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('booking_logs', function (Blueprint $table) {
$table->boolean('invoice_status')
->default(false)
->after('status');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('booking_logs', function (Blueprint $table) {
$table->dropColumn('invoice_status');
});
}
}
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIsInvoiceGeneratedToBookingLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('booking_logs', function (Blueprint $table) {
$table->boolean('is_invoice_generated')
->default(false)
->after('status');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('booking_logs', function (Blueprint $table) {
$table->dropColumn('is_invoice_generated');
});
}
}
+7 -2
View File
@@ -679,8 +679,13 @@ class DummyDataSeeder extends Seeder
// once the invoice is generated the transaction table will include 2 new transaction type TransactionType::INVOICE, TransactionType::SUPPLIER_DELIVERY
// and for documents will be generated and attached to the booking.
// once this process is complete the booking status will update to ApprovalStatus::COMPLETED
$this->createInvoiceTransactionProcessor->execute($booking);
$this->createInvoiceTransactionProcessor->execute($booking, "", null, [
'generateEInvoice' => false,
'generateEInvoiceWithNormalInvoiceTemplate' => false,
'generateEInvoiceRefund' => false,
'bookingOriginalStatus' => ApprovalStatus::COMPLETED
]
);
}
}
@@ -169,6 +169,7 @@
</template>
<script>
import registrationFormValidation from '../../../general/mixins/accounts/validation/registrationFormValidation'
import { track } from '../../../utils/tracking';
export default {
data() {
return {
@@ -205,6 +206,17 @@
},
submitForm(){
this.step === 2 ? this.submit(this.route('api.account.registration.register'), 'post', 'registrationSection', false, false) : this.changeStep('next');
},
successHandler(response){
// fire to gtag manager for fb pixel tracking: CompleteRegistration
track('exchange_complete_registration', {
content_name: this.parameters.type === 1 ? 'Company' : 'Personal',
currency: 'MYR'
});
// Call parent success handler logic from authenticationHandler mixin
this.formHandler('');
this.$store.dispatch('userAuthentication', {access_token: response.payload.access_token, redirect_url: response.payload.redirect_url});
}
},
mixins: [registrationFormValidation]
@@ -24,6 +24,7 @@
6. 01DRW - Sales Deposit by Wallet [AR PAYMENT ENTRY] → Filters by Created Date<br/>
7. 01DRF - Sales Deposit by Wallet [AR REFUND ENTRY] → Filters by Created Date<br/>
8. 01DD - Sales Deposit by Wallet [AR DEPOSIT ENTRY] → Filters by Created Date<br/>
9. WALLET TOP UP REPORT [Wallet Deposit Received] → Filters by Top Up Date<br/>
</div>
">
<i class="fa fa-info-circle"></i>
@@ -52,6 +53,7 @@
6. 01DRW - Sales Deposit by Wallet [AR PAYMENT ENTRY] → Filters by Created Date<br/>
7. 01DRF - Sales Deposit by Wallet [AR REFUND ENTRY] → Filters by Created Date<br/>
8. 01DD - Sales Deposit by Wallet [AR DEPOSIT ENTRY] → Filters by Created Date<br/>
9. WALLET TOP UP REPORT [Wallet Deposit Received] → Filters by Top Up Date<br/>
</div>
">
<i class="fa fa-info-circle"></i>
@@ -196,10 +198,10 @@ export default {
'01D - Sales Deposit Received [AR DEPOSIT ENTRY]',
'01R - RECEIVE PAYMENT [AR RECEIVE PAYMENT]',
'Credit Note Report',
//'WALLET TOP UP REPORT [Wallet Deposit Received]',
'01DRW - Sales Deposit by Wallet [AR PAYMENT ENTRY]',
'01DRF - Sales Deposit by Wallet [AR REFUND ENTRY]',
'01DD - Sales Deposit by Wallet [AR DEPOSIT ENTRY]'
'01DD - Sales Deposit by Wallet [AR DEPOSIT ENTRY]',
'WALLET TOP UP REPORT [Wallet Deposit Received]'
];
},
handleExportClick(){
@@ -215,10 +217,10 @@ export default {
'01D - Sales Deposit Received [AR DEPOSIT ENTRY]': route('api.export.transactions.receive_payment_deposit_entry'),
'01R - RECEIVE PAYMENT [AR RECEIVE PAYMENT]': route('api.export.transactions.receive_payment_for_booking'),
'Credit Note Report': route('api.export.transactions.ar_credit_note'),
//'WALLET TOP UP REPORT [Wallet Deposit Received]': route('api.export.transactions.wallet_top_up_deposit_entry'),
'01DRW - Sales Deposit by Wallet [AR PAYMENT ENTRY]': route('api.export.transactions.sales_deposit_by_wallet_payment_entry'),
'01DRF - Sales Deposit by Wallet [AR REFUND ENTRY]': route('api.export.transactions.sales_deposit_by_wallet_refund_entry'),
'01DD - Sales Deposit by Wallet [AR DEPOSIT ENTRY]': route('api.export.transactions.sales_deposit_by_wallet_deposit_entry'),
'WALLET TOP UP REPORT [Wallet Deposit Received]': route('api.export.transactions.wallet_top_up_deposit_entry'),
};
let url = `${routesMap[reportType]}?startDate=${this.parameters.startDate}&endDate=${this.parameters.endDate}`;
@@ -16,7 +16,7 @@
<p class="muted">MSIC Code</p>
<p class="m-b-0">{{ eInvoiceData.msic_code }}</p>
</div>
<div class="padding-15 bg-master-lightest">
<div class="padding-15 bg-master-lightest m-b-10">
<p class="muted">Billing Address</p>
<p class="mb-0">
<a data-toggle="collapse" href="#billingDetails" role="button" aria-expanded="false" aria-controls="billingDetails">
@@ -35,6 +35,10 @@
</ul>
</div>
</div>
<div class="padding-15 bg-master-lightest m-b-10" v-if="$store.getters.isAdmin">
<p class="muted">Requested Date</p>
<p class="m-b-0">{{ eInvoiceData.e_invoice_requested_date }} (d-m-Y)</p>
</div>
</div>
</div>
</div>
@@ -162,29 +162,6 @@
</modal-component>
</div>
</div>
<modal-component
id="modal-einvoice-request"
class="animate__animated animate__fast animate__fadeIn"
styleType="fill-in" type="requestEInvoiceA" size="large">
<e-invoice-request-form-component
class="text-center"
:section="section"
:company-id="data.company.id"
@choice-made="handleEInvoiceRequestRespond"
/>
</modal-component>
<modal-component
id="modal-einvoice-info"
class="animate__animated animate__fast animate__fadeIn"
styleType="fill-in" type="requestEInvoiceB" size="large">
<e-invoice-info-form-component
:section="section"
:company-id="data.company.id"
:company-type="data.company.type"
v-on:eInvoiceInfoUpdated="updatedEInvoiceInfo($event)"
v-on:eInvoiceChangeOfMindRequest="changeOfMindEInvoiceRequest()">
</e-invoice-info-form-component>
</modal-component>
<!-- E-Invoice - end -->
</div>
</div>
@@ -622,7 +599,7 @@
<!-- E-Invoice -->
<button id="lock-booking" class="btn btn-sm all-caps b-rad-none btn-success btn-block"
v-if="paymentMethod.id !== 'wallet' || walletOutstanding >= 0"
@click.prevent="checkPurchaseOrderRule">Lock Booking</button>
@click.prevent="checkEInvoiceAmountLimitRule">Lock Booking</button>
</div>
<modal-component id="confirm-booking-modal" v-if="calculation.total > 0" class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="paymentSummary">
<confirm-quotation-form-component v-on:cancelQuotation="cancelQuotation()" :calculation="calculation" :section="section" :id="item.id" :payment_method="paymentMethod.id" :bank_code="onlinePayment.id" :amount="amount" :company-id="item.company.id"></confirm-quotation-form-component>
@@ -630,6 +607,31 @@
</div>
</div>
</div>
<!-- E-Invoice - start -->
<modal-component
id="modal-einvoice-request"
class="animate__animated animate__fast animate__fadeIn"
styleType="fill-in" type="requestEInvoiceA" size="large">
<e-invoice-request-form-component
class="text-center"
:section="section"
:company-id="data.company.id"
@choice-made="handleEInvoiceRequestRespond"
/>
</modal-component>
<modal-component
id="modal-einvoice-info"
class="animate__animated animate__fast animate__fadeIn"
styleType="fill-in" type="requestEInvoiceB" size="large">
<e-invoice-info-form-component
:section="section"
:company-id="data.company.id"
:company-type="data.company.type"
v-on:eInvoiceInfoUpdated="updatedEInvoiceInfo($event)"
v-on:eInvoiceChangeOfMindRequest="changeOfMindEInvoiceRequest()">
</e-invoice-info-form-component>
</modal-component>
<!-- E-Invoice - end -->
</div>
</div>
</div>
@@ -740,6 +742,9 @@
$('#confirm-booking-modal').modal('show');
}
}
else if(section === this.section + 'CheckEInvoiceAmountLimitRule'){
this.checkPurchaseOrderRule();
}
else if(section === this.section + 'ChangeOfMind'){
this.$store.dispatch('reloadList', {'name': "bookingDetailSection"});
}
@@ -767,6 +772,14 @@
if(section === this.section + 'CheckEInvoiceRule' && statusCode === 422){
$('#modal-einvoice-info').modal('show');
}
else if(section === this.section + 'CheckEInvoiceAmountLimitRule' && statusCode === 422){
if(!this.data.company.e_invoice){
$('#modal-einvoice-request').modal('show');
}
else if(!this.data.company.tin){
$('#modal-einvoice-info').modal('show');
}
}
this.error = error.message;
},
makePayment(){ //E-Invoice
@@ -837,6 +850,17 @@
};
this.submit(route('api.rule.check.einvoice'), 'post', this.section + 'CheckEInvoiceRule', false, true);
},
checkEInvoiceAmountLimitRule(){
this.error = '';
this.parameters = {
booking_id: this.data.id,
company_id: this.data.company.id,
payment_method: this.paymentMethod.id,
voucherCode: this.voucherCode,
amount: this.amount
};
this.submit(route('api.rule.check.einvoice.amount-limit'), 'post', this.section + 'CheckEInvoiceAmountLimitRule', false, true);
},
checkPurchaseOrderRule(){
this.error = '';
this.parameters = {
@@ -198,7 +198,20 @@
<div class="col-12 col-md-5 pr-md-0" v-if="submitted && data.status !== 3">
<div class="row m-b-15" v-if="($store.getters.isAdmin && data.purchase_order.status === 1) || ($store.getters.isCustomer && data.purchase_order.status === 1 && $store.getters.getCompanyId === 199)" >
<div class="col">
<button class="btn btn-sm btn-block btn-success b-rad-none" @click="submit(route('api.booking.po.approval', data.id), 'post', section, true, true)">Approve Purchase Order</button>
<button class="btn btn-sm btn-block btn-success b-rad-none" :disabled="poProcessing" @click="handleRepprove()">Approve Purchase Order</button>
</div>
</div>
<div class="row m-b-15" v-if="$store.getters.isAdmin && data.purchase_order.status === 2" >
<div class="col">
<button class="btn btn-sm btn-block btn-success b-rad-none" :disabled="poProcessing" @click="handleRepprove()">
Repprove Purchase Order
<span class="badge badge-danger position-absolute" style="top:-5px; right:-5px;"
data-toggle="tooltip"
data-placement="right"
data-html="true"
data-custom-class="tooltip-report-export"
title="Allow admin to rerun of 'Approve Purchase Order' to generate invoices, conditions apply">ADMIN</span>
</button>
</div>
</div>
</div>
@@ -333,6 +346,7 @@
products: [],
files: [],
uploadFiles: false,
poProcessing: false,
}
},
validations: {
@@ -444,6 +458,9 @@
this.checkEInvoiceRule();
}
else if(section === this.section + 'CheckEInvoiceRule'){
this.checkEInvoiceAmountLimitRule();
}
else if(section === this.section + 'CheckEInvoiceAmountLimitRule'){
if(response.payload.data.isPassed){
if(this.data.service.id === 14 || this.data.service.id === 15 || this.data.service.id === 16 || this.data.service.id === 17 || this.data.service.id === 18 || this.data.service.id === 19){
this.submit(route('api.booking.banking.create', this.data.id), 'post', this.section, true, true);
@@ -459,11 +476,20 @@
}
this.updateList()
}
this.poProcessing = false;
},
errorHandler(error, statusCode, section) { //E-Invoice
if(section === this.section + 'CheckEInvoiceRule' && statusCode === 422){
$('#modal-einvoice-info').modal('show');
}
else if(section === this.section + 'CheckEInvoiceAmountLimitRule' && statusCode === 422){
if(!this.data.company.e_invoice){
$('#modal-einvoice-request').modal('show');
}
else if(!this.data.company.tin){
$('#modal-einvoice-info').modal('show');
}
}
},
addProduct(){
this.products.push({
@@ -530,6 +556,14 @@
};
this.submit(route('api.rule.check.einvoice'), 'post', this.section + 'CheckEInvoiceRule', false, true);
},
checkEInvoiceAmountLimitRule(){
this.error = '';
this.parameters = {
booking_id: this.data.id,
company_id: this.data.company.id,
};
this.submit(route('api.rule.check.einvoice.amount-limit'), 'post', this.section + 'CheckEInvoiceAmountLimitRule', false, true);
},
updatedEInvoiceInfo(info){
this.$store.dispatch('reloadList', {'name': "bookingDetailSection"});
},
@@ -540,8 +574,14 @@
};
this.parameters.e_invoice_request = false;
this.submit((this.route('api.company.einvoice.request.change')), 'post', this.section + 'ChangeOfMind', true, true);
},
handleRepprove() {
if (this.poProcessing) return;
this.poProcessing = true;
this.submit(route('api.booking.po.approval', this.data.id), 'post', this.section, true, true);
}
},
mixins: [formHandler]
}
</script>
<
@@ -11,6 +11,7 @@
<script>
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
import { track } from '../../../utils/tracking';
export default {
props:{
order_reference_no: {
@@ -26,6 +27,12 @@
},
methods: {
submitForm(){
// fire to gtag manager for fb pixel tracking: AddToCart — fired when the user clicks Confirm Order (user intent, OK even if API fails)
track('exchange_add_to_cart', {
value: Number(String(this.data.amount).replace(/,/g, '')),
currency: 'MYR'
});
this.parameters = {
company_id: this.data.company.id,
type: this.data.type,
@@ -42,6 +49,13 @@
this.submit(route('api.booking.create'), 'post', this.section, true, true)
},
successHandler(response){
// fire to gtag manager for fb pixel tracking: InitiateCheckout — booking created
track('exchange_checkout', {
value: Number(String(this.data.amount).replace(/,/g, '')),
currency: 'MYR',
booking_id: response.payload.data.marking
});
window.location.href = this.route('booking.details', response.payload.data.marking)
}
@@ -6,7 +6,7 @@
<div class="col-md col-sm-12 m-b-20">
<div class="row" v-if="booking">
<div class="col" v-if="showDownloadPDFButtons">
<div class="row m-b-50" v-if="booking.status === 3 || isBookingWithAFullRefund">
<div class="row m-b-50" v-if="(booking.status === 3 || booking.invoice_status === 2) || isBookingWithAFullRefund">
<div class="col no-padding">
<div class="row">
<div class="col">
@@ -122,7 +122,7 @@
</div>
</div>
</div>
<div class="row m-b-10" v-if="booking.status !== 3 && !isBookingWithAFullRefund">
<div class="row m-b-10" v-else>
<div class="col">
<div class="row text-center m-b-15">
<div class="col">
+13
View File
@@ -0,0 +1,13 @@
// resources/assets/vue/utils/tracking.js
export function track(event, payload = {}) {
if (typeof window === 'undefined') return;
// console.log('tracking', event, payload);
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event,
...payload,
});
}
@@ -103,4 +103,21 @@
</div>
</div>
@endsection
@endsection
@push('scripts')
@if($status === 2 && isset($transaction) && $transaction)
<script>
(function() {
// fire to gtag manager for fb pixel tracking: exchange_purchase
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'exchange_purchase',
transaction_id: @json($transaction->payment_reference ?? ''),
value: {{ number_format((float)($transaction->amount ?? 0), 2, '.', '') }},
currency: 'MYR'
});
})();
</script>
@endif
@endpush
+1
View File
@@ -9,4 +9,5 @@ Route::prefix('rule')
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');
Route::post('/check/e-invoice/amount-limit', [CheckRuleController::class, 'checkEInvoiceAmountLimitRule'])->name('check.einvoice.amount-limit');
});