Merge branch 'vapor/production' into dillon/90-e-invoice-f

This commit is contained in:
Dillon Ngo
2025-08-13 02:55:16 +08:00
22 changed files with 648 additions and 40 deletions
@@ -0,0 +1,44 @@
<?php
namespace App\Classes\Jobs\Commands\V2;
use App\Classes\Modules\Bookings\Processors\RegenerateInvoiceBookingProcessor;
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 App\Models\Booking;
use Illuminate\Support\Facades\Log;
class OneTimeBatchProcessEInvoicesV2CommandJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var Booking */
private $booking;
/**
* OneTimeBatchProcessEInvoicesV2CommandJob constructor.
* @param Booking $booking
*/
public function __construct(Booking $booking)
{
$this->booking = $booking;
}
public function handle()
{
Log::info(Carbon::now() . ': Start job - Processing single booking for E-Invoices for July 2025.');
$start = new Carbon();
$isAllowNormalInvoice = true;
(App()->make(RegenerateInvoiceBookingProcessor::class))->execute($this->booking, $isAllowNormalInvoice);
$end = new Carbon();
$elapsedTime = $start->diff($end)->format('%H:%I:%S');
Log::info(Carbon::now() . ': End job - Processing single booking for E-Invoices for July 2025. ElapsedTime: ' . $elapsedTime . '.');
}
}
@@ -8,6 +8,7 @@ use App\Classes\Jobs\Commands\V2\ProcessBookingForEInvoiceV2CommandJob;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Models\Booking;
use App\Classes\Modules\Bookings\Processors\RegenerateInvoiceBookingProcessor;
use App\Classes\ValueObjects\Constants\KVPKey;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
@@ -27,7 +28,7 @@ class BatchBookingsGenerateEInvoiceLogic extends AbstractControllerLogic
return [
'title' => 'Generate Bookings E-Invoices',
'message' => sprintf(
'You have successfully submitted %d booking%s for E-Invoices.',
'You have successfully submitted %d booking%s for E-Invoices processing.',
$this->processedCount,
$this->processedCount === 1 ? '' : 's'
),
@@ -83,10 +84,10 @@ class BatchBookingsGenerateEInvoiceLogic extends AbstractControllerLogic
$bookings = Booking::where('status', ApprovalStatus::COMPLETED)
->whereBetween('created_at', [$startDate, $endDate])
->whereHas('attributesKVP', function (Builder $query) {
$query->where('key', 'AUTOCOUNT_DOCNO');
$query->where('key', KVPKey::AUTOCOUNT_DOCNO);
})
->with(['attributesKVP' => function ($query) {
$query->where('key', 'AUTOCOUNT_DOCNO');
$query->where('key', KVPKey::AUTOCOUNT_DOCNO);
}])
->get();
@@ -69,8 +69,9 @@ class RegenerateInvoiceBookingLogic extends AbstractControllerLogic
'with_transactions' => true
]
);
$normalInvoice = $request->input('normal_invoice', false);
$this->regenerateInvoiceBookingProcessor->execute($booking);
$this->regenerateInvoiceBookingProcessor->execute($booking, $normalInvoice);
return $this->resourceResponse(new BookingResource($booking));
}
@@ -14,6 +14,7 @@ use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\Booking;
use App\Models\Transaction;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
class RegenerateInvoiceBookingProcessor
{
@@ -48,7 +49,7 @@ class RegenerateInvoiceBookingProcessor
$this->createInvoiceTransactionProcessor = $createInvoiceTransactionProcessor;
}
public function execute(Booking $booking)
public function execute(Booking $booking, bool $isAllowNormalInvoice = false)
{
$this->updatesBookingStatus->execute($booking, ApprovalStatus::APPROVED);
@@ -58,6 +59,8 @@ class RegenerateInvoiceBookingProcessor
->orderBy('created_at', 'asc')
->first();
Log::info('RegenerateInvoiceBookingProcessor booking: ' . json_encode($booking->marking));
// get the first bill_no
if($firstInvoice){
$firstBillNo = $firstInvoice->bill_no;
@@ -90,10 +93,10 @@ class RegenerateInvoiceBookingProcessor
$this->deletesDocument->execute($row);
}
$this->createInvoiceTransactionProcessor->execute($booking, $firstBillNo, true);
$this->createInvoiceTransactionProcessor->execute($booking, $firstBillNo, true, $isAllowNormalInvoice);
}
else {
$this->createInvoiceTransactionProcessor->execute($booking);
$this->createInvoiceTransactionProcessor->execute($booking, "", false, $isAllowNormalInvoice);
}
}
}
@@ -15,4 +15,14 @@ class CalculatesBookingPaidAmount
return $booking->transactions()->payments()->complete()->sum('original_amount');
}
public function executeUntilDate(Booking $booking, Carbon $cutOffDate = null){
$amount = $booking->transactions()->payments()->complete();
if($cutOffDate){
$amount->where('created_at', '<=', $cutOffDate);
}
return $amount->sum('original_amount');
}
}
@@ -0,0 +1,127 @@
<?php
namespace App\Classes\Modules\Exports\Services;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Models\KeyValuePair;
use App\Models\Transaction;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class ExportsARCreditNoteReport implements FromQuery, WithHeadings, WithHeadingRow, WithMapping, ShouldAutoSize
{
use Exportable;
protected $startDate;
protected $endDate;
public function __construct($startDate = null, $endDate = null) {
$this->startDate = $startDate ? Carbon::parse($startDate)->startOfDay() : Carbon::now()->subMonths(1);
$this->endDate = $endDate ? Carbon::parse($endDate)->endOfDay() : Carbon::now();
}
public function headings(): array
{
return [
'DocNo',
'DocDate',
'DebtorCode',
'Ref',
'Description',
'Reason',
'DeptNo',
'Qty',
'UnitPrice',
'AccNo',
'submiteinvoice',
'ConsolidatedEinvoice',
'KnockOffDocNo',
'KnockOffAmt',
];
}
/**
* @return \Illuminate\Support\Collection|mixed
*/
public function query()
{
$type = TransactionType::CREDIT_NOTE;
$query = Transaction::query();
$query->where('type', $type);
// $query->where('owner_type', Wallet::class);
$query->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]);
$query->whereBetween('created_at', [$this->startDate, $this->endDate]);
return $query;
}
/**
* @param Transaction $transaction
* @return array
*/
public function map($transaction): array
{
$booking = null;
$autoCountSalesInvoiceId = null;
$formattedDocumentDate = null;
$refundRemark = null;
$company = $transaction->owner->owner;
$kvps = KeyValuePair::where('value', $transaction->id)
->where('key', 'App\Models\Transaction')
->orderByDesc('created_at')
->get();
foreach ($kvps as $kvp) {
if ($kvp && $kvp->owner && $kvp->owner->owner && $kvp->owner->owner->type === 1) {
$refundTransaction = $kvp->owner;
$refundRemark = $refundTransaction->remarks && $refundTransaction->remarks->first() ? $refundTransaction->remarks->first()->content : null;
$booking = $refundTransaction->owner->booking;
if($booking){
$metadata = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO)->first();
if($metadata){
$autoCountSalesInvoiceId = $metadata->value;
}
break;
}
}
}
$documentDate = $transaction->created_at; //NEW 2025: default in case there is no approval date
$metadata = $transaction->attributesKVP()->where('key', KVPKey::CREDIT_NOTE_APPROVAL_DATE)->latest('created_at')->first();
if($metadata){
$documentDate = Carbon::parse($metadata->value);
}
if($company->e_invoice === 1){
$documentDate = $documentDate->copy()->endOfMonth();
}
$formattedDocumentDate = Carbon::parse($documentDate)->format('m/d/Y');
return [
'<<New>>', //DocNo
$formattedDocumentDate, //DocDate
$company->debtor, //DebtorCode
$booking ? $booking->marking : '', //Ref
$refundRemark ?? '', //Description
$refundRemark ?? '', //Reason
'C', //DeptNo
'1', //Qty
number_format($transaction->amount, 2), //UnitPrice
'511-0000', //AccNo
'F', //submiteinvoice
$company->e_invoice ? 'F' : 'T', //ConsolidatedEinvoice
$autoCountSalesInvoiceId ?? '', //KnockOffDocNo
number_format($transaction->amount, 2), //KnockOffAmt
];
}
}
@@ -71,6 +71,8 @@ class ExportsNullDebtors implements FromQuery, WithHeadings, WithHeadingRow, Wit
*/
public function map($company): array
{
$employee = $company->employees()->orderBy('id', 'DESC')->first();
return [
'<<New>>', // Code
'300-0000', // DebtorControlAcc
@@ -89,7 +91,7 @@ class ExportsNullDebtors implements FromQuery, WithHeadings, WithHeadingRow, Wit
'', // DeliverAddr2
'', // DeliverAddr3
'', // DeliverPostCode
'', // EmailAddress
$employee->email, // EmailAddress
'', // Attention
'', // Phone1
'', // Phone2
@@ -10,6 +10,7 @@ use App\Classes\Modules\Accounts\Services\UpdatesKeyValuePair;
use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject;
use App\Classes\Modules\Accounts\DataTransferObjects\KeyValuePairObject;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Models\Booking;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -127,11 +128,13 @@ class ImportExcelLogic extends AbstractControllerLogic
]);
$booking = Booking::where('marking', $ref)->first();
if($docNo != "<<New>>"){
$this->updateOrCreateKeyValuePair($booking, "AUTOCOUNT_DOCNO", $docNo);
}
if($eInvoiceValidationLink){
$this->updateOrCreateKeyValuePair($booking, "AUTOCOUNT_EINVOICE_VALIDATION_LINK", $eInvoiceValidationLink);
if($booking){
if($docNo != "" && $docNo != "<<New>>"){
$this->updateOrCreateKeyValuePair($booking, KVPKey::AUTOCOUNT_DOCNO, $docNo);
}
if($eInvoiceValidationLink){
$this->updateOrCreateKeyValuePair($booking, KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK, $eInvoiceValidationLink);
}
}
}
}
@@ -10,6 +10,7 @@ use App\Classes\Modules\Transactions\Services\FetchesTransaction;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\General\AWSS3Helper;
use App\Classes\ValueObjects\Constants\DocumentType;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Models\KeyValuePair;
use App\Models\Transaction;
use Carbon\Carbon;
@@ -49,9 +50,9 @@ class GenerateCreditNotePdfV2Logic
if($transaction->type === TransactionType::REFUND){
//Retrieve TransactionType::CREDIT_NOTE
$booking = $transaction->owner->booking;
$kvp = $transaction->attributesKVP()->latest()->first();
$kvp = $transaction->attributesKVP()->where('key', KVPKey::TRANSACTION_MODEL_CLASS)->latest()->first();
if($kvp){
if($kvp->key === 'App\Models\Transaction'){
if($kvp->key === KVPKey::TRANSACTION_MODEL_CLASS){
$transaction = $this->fetchesTransaction->execute(['id' => $kvp->value ]);
}
}
@@ -62,7 +63,7 @@ class GenerateCreditNotePdfV2Logic
$pdfTemplateName = 'pages.pdfs.credit_note'; //default
//For New Cases with e-invoice: Retrieve the refund transaction for this credit note
$kvp = KeyValuePair::where('key', 'App\Models\Transaction')->where('value', $transaction->id)->first();
$kvp = KeyValuePair::where('key', KVPKey::TRANSACTION_MODEL_CLASS)->where('value', $transaction->id)->first();
if($kvp){
$kvpOwner = $kvp->owner;
if($kvpOwner && $kvpOwner instanceof Transaction && $kvpOwner->type === TransactionType::REFUND){
@@ -71,7 +72,12 @@ class GenerateCreditNotePdfV2Logic
}
}
$date = $transaction->created_at;
$date = $transaction->created_at; //NEW 2025: default in case there is no approval date
$metadata = $transaction->attributesKVP()->where('key', KVPKey::CREDIT_NOTE_APPROVAL_DATE)->latest('created_at')->first();
if($metadata){
$date = Carbon::parse($metadata->value);
}
$supplier = $this->fetchesCompany->execute(['id' => $transaction->receiver]);
$brn = $supplier->documents->where('document_type', DocumentType::SSM_REGISTRATION)->first();
@@ -82,13 +88,34 @@ class GenerateCreditNotePdfV2Logic
if ($bookingCreatedDate->isAfter($eInvoiceStartDate)) {
$eInvoiceStarted = true;
}
// $eInvoiceStarted = false; //cief todo: 90 - for testing
if($eInvoiceStarted) {
$eInvoiceStarted = false; //reset to re-evaluate second time
$autoCountInvoiceId = '';
$autoCountEInvoiceValidationLink = '';
$metadata = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO)->first();
if($metadata){
$autoCountInvoiceId = $metadata->value;
}
$metadata = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK)->first();
if($metadata){
$autoCountEInvoiceValidationLink = $metadata->value;
}
Log::info('autoCountInvoiceId: ' . $autoCountInvoiceId);
Log::info('autoCountEInvoiceValidationLink: ' . $autoCountEInvoiceValidationLink);
if($autoCountInvoiceId && $autoCountEInvoiceValidationLink){
$eInvoiceStarted = true;
}
}
if($eInvoiceStarted)
{
if($supplier->e_invoice === 1){
Log::info('Based on booking created date, E-Credit Note started and company wants e-invoice ' . json_encode($booking));
$date = $booking->updated_at->copy()->endOfMonth();
$date = $date->copy()->endOfMonth();
$pdfTemplateName = 'pages.pdfs.e_credit_note';
}
else{
@@ -97,7 +124,7 @@ class GenerateCreditNotePdfV2Logic
}
}
else{
Log::info('Based on booking created date, E-Credit Note not yet started');
Log::info('Based on booking created date, E-Credit Note not yet started. / Not Yet Ready.');
}
$pdf = LaravelMpdf::loadView($pdfTemplateName, ['transaction' => $transaction, 'booking' => $booking, 'supplier' => $supplier, 'date' => $date, 'brn' => $brn,]);
@@ -7,6 +7,7 @@ use App\Classes\Modules\Documents\Services\CreatesFiles;
use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\DocumentType;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\Booking;
use App\Models\Document;
@@ -43,7 +44,7 @@ class CreateInvoiceDocumentProcessor
* @return void
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute($transaction, $purchaseOrder, $supplier, $document_type, $voucherRedemption = null)
public function execute($transaction, $purchaseOrder, $supplier, $document_type, $voucherRedemption = null, $isAllowNormalInvoice = false)
{
// calculate current Paid Amount
$booking = $transaction->owner_type == Booking::class ? $transaction->owner : null;
@@ -62,17 +63,17 @@ class CreateInvoiceDocumentProcessor
if ($bookingCreatedDate->isAfter($eInvoiceStartDate)) {
$lastPaymentTransaction = $booking->transactions()->where('type', TransactionType::PAYMENT)->whereIn('status', [ApprovalStatus::COMPLETED, ApprovalStatus::APPROVED])->latest()->first();
$documentDate = $lastPaymentTransaction->created_at;
if(Carbon::parse($booking->updated_at)->isAfter($lastPaymentTransaction->created_at)){
$documentDate = $booking->updated_at;
}
// if(Carbon::parse($booking->updated_at)->isAfter($lastPaymentTransaction->created_at)){ //cief todo: 90 - Batch generate E-Invoice date incorrect
// $documentDate = $booking->updated_at;
// }
}
if($document_type === DocumentType::EINVOICE){
$metadata = $booking->attributesKVP()->where('key', 'AUTOCOUNT_DOCNO')->first();
$metadata = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO)->first();
if($metadata){
$autoCountInvoiceId = $metadata->value;
}
$metadata = $booking->attributesKVP()->where('key', 'AUTOCOUNT_EINVOICE_VALIDATION_LINK')->first();
$metadata = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_EINVOICE_VALIDATION_LINK)->first();
if($metadata){
$autoCountEInvoiceValidationLink = $metadata->value;
}
@@ -87,6 +88,12 @@ class CreateInvoiceDocumentProcessor
$lowercaseDocumentType = strtolower($document_type);
if($document_type === DocumentType::EINVOICE){ //July 2025 workaround generate normal invoice instead of E-Invoice
if($isAllowNormalInvoice){
$lowercaseDocumentType = strtolower(DocumentType::INVOICE);
}
}
$order_pdf = LaravelMpdf::loadView('pages.pdfs.' . $lowercaseDocumentType,
[
'transaction' => $transaction,
@@ -21,6 +21,7 @@ use App\Classes\ValueObjects\Constants\DocumentType;
use App\Models\Booking;
use App\Models\SegmentConstant;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class CreateInvoiceTransactionProcessor
{
@@ -89,7 +90,7 @@ class CreateInvoiceTransactionProcessor
* @return void
* @throws MalformedRequestException
*/
public function execute(Booking $booking, String $invoiceNo= "", bool $isAllowEInvoice = false)
public function execute(Booking $booking, String $invoiceNo= "", bool $isAllowEInvoice = false, bool $isAllowNormalInvoice = false)
{
if ($booking->status === ApprovalStatus::COMPLETED) {
@@ -137,11 +138,18 @@ class CreateInvoiceTransactionProcessor
}
// $eInvoice = true; //cief todo: 90 - for testing
if($isAllowNormalInvoice){
$invoiceNo = ""; //July 2025 workaround generate normal invoice instead of E-Invoice
}
if($invoiceNo){
$billNumber = $invoiceNo;
}
else{
$billNUmberPrefix = $eInvoice ? 'EINV-' : 'INV-';
if($isAllowNormalInvoice){
$billNUmberPrefix = 'INV-'; //July 2025 workaround generate normal invoice instead of E-Invoice
}
$billNumber = $this->generatesTransactionBillNumber->execute($billNUmberPrefix);
}
@@ -189,7 +197,7 @@ class CreateInvoiceTransactionProcessor
if ($eInvoice)
{
if($isAllowEInvoice){
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::EINVOICE, $voucherRedemption);
$this->invoiceDocumentProcessor->execute($invoice_transaction, $purchaseOrder, $supplier, DocumentType::EINVOICE, $voucherRedemption, $isAllowNormalInvoice);
}
}
// invoice
@@ -16,6 +16,8 @@ use App\Classes\Modules\Wallets\DataTransferObjects\WalletObject;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject;
use App\Classes\Modules\Transactions\Services\GeneratesTransactionBillNumber;
use App\Classes\Modules\Accounts\Services\CreatesKeyValuePair;
use App\Classes\Modules\Accounts\Services\UpdatesKeyValuePair;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Models\Transaction;
class CreditWalletProcessor
@@ -38,6 +40,10 @@ class CreditWalletProcessor
/** @var CreatesKeyValuePair */
private $createsKeyValuePair;
/** @var UpdatesKeyValuePair */
private $updatesKeyValuePair;
/**
* CreateWalletLogic constructor.
* @param GeneratesWalletCode $generatesWalletCode
@@ -46,6 +52,7 @@ class CreditWalletProcessor
* @param CreatesTransaction $createsTransaction
* @param UpdatesWallet $updatesWallet
* @param CreatesKeyValuePair $createsKeyValuePair
* @param UpdatesKeyValuePair $updatesKeyValuePair
*/
public function __construct(
GeneratesWalletCode $generatesWalletCode,
@@ -53,7 +60,8 @@ class CreditWalletProcessor
GeneratesTransactionBillNumber $generatesTransactionBillNumber,
CreatesTransaction $createsTransaction,
UpdatesWallet $updatesWallet,
CreatesKeyValuePair $createsKeyValuePair
CreatesKeyValuePair $createsKeyValuePair,
UpdatesKeyValuePair $updatesKeyValuePair
)
{
$this->generatesWalletCode = $generatesWalletCode;
@@ -62,6 +70,7 @@ class CreditWalletProcessor
$this->createsTransaction = $createsTransaction;
$this->updatesWallet = $updatesWallet;
$this->createsKeyValuePair = $createsKeyValuePair;
$this->updatesKeyValuePair = $updatesKeyValuePair;
}
@@ -86,9 +95,15 @@ class CreditWalletProcessor
$billNumber = $this->generatesTransactionBillNumber->execute($transactionType === 2 ? 'DEBIT-NOTE-' : 'CREDIT-NOTE-');
$transaction_object = new TransactionObject($billNumber, $transactionType === 2 ? TransactionType::DEBIT_NOTE : TransactionType::CREDIT_NOTE, 1, $wallet->owner->id, 1, PaymentMethodType::CASH, $amount, $amount, 1, 1, 1, 0, 0, null, ApprovalStatus::APPROVED, [], $reference);
$status = ApprovalStatus::APPROVED;
$transaction_object = new TransactionObject($billNumber, $transactionType === 2 ? TransactionType::DEBIT_NOTE : TransactionType::CREDIT_NOTE, 1, $wallet->owner->id, 1, PaymentMethodType::CASH, $amount, $amount, 1, 1, 1, 0, 0, null, $status, [], $reference);
$transaction = $this->createsTransaction->execute($wallet, $transaction_object);
if($status === ApprovalStatus::APPROVED){
$this->updateOrCreateKeyValuePair($transaction, KVPKey::CREDIT_NOTE_APPROVAL_DATE, now());
}
$updateWalletAmount = $transactionType === 2 ? ($wallet->amount - $transaction->amount) : ($wallet->amount + $transaction->amount);
$walletObject = new WalletObject($wallet->owner->id, $wallet->currency_id, $wallet->code, $updateWalletAmount);
@@ -107,4 +122,16 @@ class CreditWalletProcessor
}
return $wallet;
}
private function updateOrCreateKeyValuePair($transaction, $key, $value)
{
$keyValuePairObject = new KeyValuePairObject($key, $value);
$metadata = $transaction->attributesKVP()->where('key', $key)->first();
if ($metadata) {
$this->updatesKeyValuePair->execute($metadata, $keyValuePairObject);
} else {
$this->createsKeyValuePair->execute($transaction, $keyValuePairObject);
}
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Classes\ValueObjects\Constants;
class KVPKey
{
public const AUTOCOUNT_DOCNO = 'AUTOCOUNT_DOCNO';
public const AUTOCOUNT_EINVOICE_VALIDATION_LINK = 'AUTOCOUNT_EINVOICE_VALIDATION_LINK';
public const CREDIT_NOTE_APPROVAL_DATE = 'CREDIT_NOTE_APPROVAL_DATE';
public const TRANSACTION_MODEL_CLASS = 'App\Models\Transaction';
}
@@ -0,0 +1,81 @@
<?php
namespace App\Console\Commands\V2;
use App\Classes\Jobs\Commands\V2\OneTimeBatchProcessEInvoicesV2CommandJob;
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 OneTimeBatchProcessEInvoicesV2Command extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'one-time-batch-process-einvoices-command';
/**
* The console command description.
*
* @var string
*/
protected $description = 'One time batch process E-Invoices for July 2025';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
return;
$startDate = Carbon::create(2025, 7, 1)->startOfDay();
$endDate = Carbon::create(2025, 7, 31)->endOfDay();
$bookings = Booking::where('status', ApprovalStatus::COMPLETED)
->whereBetween('created_at', [$startDate, $endDate])
->get();
$count = 0;
foreach ($bookings as $booking) {
$firstInvoice = $booking->transactions()
->whereIn('type', [TransactionType::INVOICE])
->withTrashed()
->whereBetween('created_at', [$startDate, $endDate])
->orderBy('created_at', 'asc')
->first();
$normalInvoice = $booking->documents()->where('document_type', DocumentType::INVOICE)->first();
$eInvoice = $booking->documents()->where('document_type', DocumentType::EINVOICE)->first();
// if($firstInvoice && !$normalInvoice && !$eInvoice){
if(!$firstInvoice && !$normalInvoice && !$eInvoice){
OneTimeBatchProcessEInvoicesV2CommandJob::dispatch($booking);
$count++;
Log::info('Processed: ' . $count);
Log::info('Booking ID: ' . $booking->marking . ' | created_at: ' . $booking->created_at);
}
}
}
}
@@ -8,6 +8,7 @@ use Illuminate\Http\Request;
use Maatwebsite\Excel\Excel;
use Illuminate\Support\Facades\Storage;
use App\Classes\General\AWSS3Helper;
use App\Classes\Modules\Exports\Services\ExportsARCreditNoteReport;
use App\Classes\Modules\Exports\Services\ExportsCompanies;
use Carbon\Carbon;
@@ -83,4 +84,39 @@ class ExportController
}
return $response;
}
public function arCreditNote(Request $request){
$validated = $request->validate([
'startDate' => 'nullable|date_format:d-m-Y',
'endDate' => 'nullable|date_format:d-m-Y|after_or_equal:startDate',
]);
$startDate = null;
$endDate = null;
if (isset($validated['startDate']) && $validated['startDate']) {
$startDate = Carbon::createFromFormat('d-m-Y', $validated['startDate'])->startOfDay();
} else {
$startDate = Carbon::now()->subMonths(1)->startOfDay();
}
if (isset($validated['endDate']) && $validated['endDate']) {
$endDate = Carbon::createFromFormat('d-m-Y', $validated['endDate'])->endOfDay();
} else {
$endDate = Carbon::now()->endOfDay();
}
$exportsTransactions = new ExportsARCreditNoteReport($startDate, $endDate);
$exportFileName = 'Exchange - AR Credit Note Report.xls';
$filesystemDriver = Storage::getDefaultDriver();
if($filesystemDriver === 's3'){
return response([ 'src' => AWSS3Helper::S3Exportable($exportFileName, $exportsTransactions) ]);
}
else{
$response = $exportsTransactions->download($exportFileName, Excel::XLS, ['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']);
ob_end_clean();
}
return $response;
}
}
@@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Reports;
use App\Classes\Modules\Bookings\Services\CalculatesBookingPaidAmount;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use Illuminate\Http\Request;
use Carbon\Carbon;
class UnfinishedPaymentOrders extends Controller
{
public function execute(Request $request)
{
$page = (int) $request->input('page', 1);
$perPage = 2500;
$offset = ($page - 1) * $perPage;
$cutOffDate = $request->cut_off_date ? Carbon::parse($request->cut_off_date) : null;
$baseQuery = Booking::with('company')
->when($cutOffDate, fn($q) => $q->where('created_at', '<=', $cutOffDate))
->orderByDesc('id');
$total = $baseQuery->count();
$bookings = $baseQuery->offset($offset)->limit($perPage)->get();
$calculator = new CalculatesBookingPaidAmount();
$filtered = $bookings->filter(function ($b) use ($calculator, $cutOffDate) {
$paid = $calculator->executeUntilDate($b, $cutOffDate);
$outstanding = $b->fix_amount - $paid;
return $paid > 0 && $outstanding > 0;
});
return response()->json([
'success' => true,
'current_page' => $page,
'next_page' => ($offset + $perPage < $total) ? $page + 1 : null,
'count' => $filtered->count(),
'data' => $filtered->map(function ($b) use ($calculator, $cutOffDate) {
$paid = $calculator->executeUntilDate($b, $cutOffDate);
return [
'id' => $b->id,
'order_ref' => $b->marking ?? $b->id,
'booking_amount' => number_format($b->fix_amount, 2),
'paid_amount' => number_format($paid, 2),
'outstanding_amount' => number_format($b->fix_amount - $paid, 2),
'customer' => optional($b->company)->reference,
'created_at' => $b->created_at->toDateTimeString(),
];
})->values(),
]);
}
public function loadView(Request $request)
{
$cutOffDateString = $request->cut_off_date ?? '';
$cutOffDateParsed = $cutOffDateString ? Carbon::parse($cutOffDateString)->toDateString() : '-';
echo <<<HTML
<p>Cut Off Date: {$cutOffDateParsed}</p>
<div class="log">🔄 Processing... Total 0</div>
<br>
<style>
table {
border-collapse: collapse;
width: 100%;
font-family: Arial, sans-serif;
font-size: 14px;
}
th, td {
padding: 6px 10px;
border: 1px solid #ccc;
}
thead {
background: #f1f1f1;
}
.log {
margin-top: 15px;
font-family: monospace;
white-space: pre-line;
}
</style>
<table id="results-table">
<thead>
<tr>
<th>Order Ref</th>
<th>Booking Amount</th>
<th>Paid Amount</th>
<th>Outstanding Amount</th>
<th>Customer</th>
<th>Order Created Date</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script>
let currentPage = 1;
const cutOffDate = encodeURIComponent("{$cutOffDateString}");
const tableBody = document.querySelector('#results-table tbody');
const log = document.querySelector('.log');
const bookingUrlTemplate = "/transfer/__ORDER_REF__";
const customerUrlTemplate = "/customer/__ORDER_REF__";
let totalRows = 0;
function updateLogMessage() {
log.textContent = `🔄 Processing... Total \${totalRows}`;
}
function runNextBatch() {
const url = `/run-batch-unfinished-payment-orders?page=\${currentPage}&cut_off_date=\${cutOffDate}`;
fetch(url)
.then(res => {
if (!res.ok) throw new Error("404 or server error");
return res.json();
})
.then(data => {
if (data.success) {
totalRows += data.count;
updateLogMessage();
data.data.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td><a href="\${bookingUrlTemplate.replace('__ORDER_REF__', item.order_ref)}" target="_blank">\${item.order_ref}</a></td>
<td>\${item.booking_amount}</td>
<td>\${item.paid_amount}</td>
<td>\${item.outstanding_amount}</td>
<td><a href="\${customerUrlTemplate.replace('__ORDER_REF__', item.customer)}" target="_blank">\${item.customer ?? '-'}</a></td>
<td>\${item.created_at}</td>
`;
tableBody.appendChild(row);
});
if (data.next_page) {
currentPage = data.next_page;
runNextBatch();
} else {
log.textContent = `✅ Completed. Total \${totalRows}`;
}
}
})
.catch(err => {
log.textContent = "❌ Error: " + err + "\\n";
});
}
runNextBatch();
</script>
HTML;
}
}
@@ -101,13 +101,6 @@ export default {
generateEInvoicesUrl: null,
}
},
mounted(){
switch(this.section) {
case 'paymentsReportSection':
this.parameters.reportType = 'Sales Invoice Report'
break;
}
},
validations: {
parameters: {
startDate: {
@@ -126,6 +119,7 @@ export default {
return [
'Sales Invoice Report',
'Customers Report',
'AR Credit Note Report',
];
},
handleExportClick(){
@@ -137,6 +131,7 @@ export default {
const routesMap = {
'Sales Invoice Report': route('api.export.bookings.sales-invoices'),
'Customers Report': route('api.export.companies.customers-data'),
'AR Credit Note Report': route('api.export.transactions.ar-credit-note'),
};
let url = `${routesMap[reportType]}?startDate=${this.parameters.startDate}&endDate=${this.parameters.endDate}`;
@@ -29,6 +29,7 @@
export default {
methods: {
submitForm() {
this.parameters.normal_invoice = false;
this.submit(this.route('api.booking.einvoice.regenerate', this.data.id), 'post', this.section, true, true);
},
successHandler(){
@@ -0,0 +1,42 @@
<template>
<div class="row">
<div class="col bg-white padding-40 b-rad-lg">
<loading-component style="height: 300px; top: 0;" key="1" color="success" v-show="isLoading" ></loading-component>
<div class="row justify-content-center" v-show="!isLoading">
<div class="col">
<div class="row m-b-20">
<div class="col">
<h3 class="all-caps">Are you Sure?</h3>
<div class="fs-11">Are you sure you want to regenerate normal invoice instead of e-invoice for this payment?</div>
</div>
</div>
<div class="row">
<div class="col p-r-5">
<div class="btn btn-sm btn-success btn-block b-rad-none" data-dismiss="modal">Cancel</div>
</div>
<div class="col p-l-5">
<div class="btn btn-sm btn-danger btn-block b-rad-none" @click="submitForm()">Confirm</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import componentHandler from '../../../general/mixins/componentHandler';
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
export default {
methods: {
submitForm() {
this.parameters.normal_invoice = true;
this.submit(this.route('api.booking.einvoice.regenerate', this.data.id), 'post', this.section, true, true);
},
successHandler(){
this.closeModal();
this.$store.dispatch('reloadList', {'name': "bookingDetailSection"});
}
},
mixins: [componentHandler, ModalFormHandler]
}
</script>
@@ -334,7 +334,7 @@
</general-confirmation-form-component>
</modal-component>
</div>
<div class="row m-t-15" v-if="booking.status === 3 && ($store.getters.isSuperAdmin || ($store.getters.isCustomer && $store.getters.getCompanyId === 199)) && booking.einvoice">
<div class="row m-t-15" v-if="booking.status === 3 && ($store.getters.isSuperAdmin || ($store.getters.isCustomer && $store.getters.getCompanyId === 199)) && booking.einvoice">
<div class="col-sm col-md-auto">
<div class="btn btn-sm btn block all-caps b-rad-none btn-danger pointer requestModal equal-width-button" data-type="regenerateEInvoice">Regenerate E-Invoice</div>
</div>
@@ -342,6 +342,14 @@
<regenerate-e-invoice-component :data="booking" :section="section" class="text-center"></regenerate-e-invoice-component>
</modal-component>
</div>
<div class="row m-t-15" v-if="booking.status === 3 && ($store.getters.isSuperAdmin || ($store.getters.isCustomer && $store.getters.getCompanyId === 199)) && booking.einvoice">
<div class="col-sm col-md-auto">
<div class="btn btn-sm btn block all-caps b-rad-none btn-danger pointer requestModal equal-width-button" data-type="regenerateNormalInvoiceEInvoice">Regenerate Normal Inv</div>
</div>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="regenerateNormalInvoiceEInvoice">
<regenerate-normal-invoice-e-invoice-component :data="booking" :section="section" class="text-center"></regenerate-normal-invoice-e-invoice-component>
</modal-component>
</div>
</div>
<div class="col-12 col-sm-12 col-md-5 mt-3 mt-sm-0">
<!-- <booking-payment-quotation-component :data="booking" :section="section"></booking-payment-quotation-component> -->
@@ -497,7 +505,7 @@
</div>
</div>
</div>
<div class="row m-t-5" v-if="$store.getters.isAdmin">
<div class="row m-t-15" v-if="$store.getters.isAdmin">
<div class="col">
<div class="btn btn-xs all-caps b-rad-none btn-warning pointer requestModal equal-width-button" data-type="changeBookingOwner">Change Booking Owner</div>
<modal-component type="changeBookingOwner">
@@ -614,7 +622,7 @@
</script>
<style scoped>
.equal-width-button {
min-width: 180px;
min-width: 200px;
display: inline-block;
text-align: center;
}
+3
View File
@@ -12,6 +12,9 @@ Route::group(['prefix' => 'export', 'as' => 'export.', 'namespace' => 'Exports']
Route::group(['prefix' => 'companies', 'as' => 'companies.'], function () {
Route::get('/customers-data', [ExportController::class, 'companies'])->name('customers-data');
});
Route::group(['prefix' => 'transactions', 'as' => 'transactions.'], function () {
Route::get('/ar-credit-note', [ExportController::class, 'arCreditNote'])->name('ar-credit-note');
});
});
Route::group(['prefix' => 'import', 'as' => 'import.', 'namespace' => 'Imports'], function () {
+10
View File
@@ -36,6 +36,7 @@ use App\Classes\Modules\Transactions\Services\DeletesTransaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Classes\General\AWSS3Helper;
use App\Http\Controllers\Reports\UnfinishedPaymentOrders;
use Illuminate\Support\Facades\File;
@@ -1369,3 +1370,12 @@ Route::get('/test-test', function () {
Route::get('/maintenance', function () {
return response()->view('errors.503', [], 503);
});
// web route to view the result
Route::get('/preview-unfinished-payment-orders', function (Request $request) {
return (new UnfinishedPaymentOrders())->loadView($request);
});
// web route to run the logic
Route::get('/run-batch-unfinished-payment-orders', function (Request $request) {
return (new UnfinishedPaymentOrders())->execute($request);
});