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

This commit is contained in:
Dillon Ngo
2025-08-18 21:52:48 +08:00
24 changed files with 664 additions and 45 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,9 +8,10 @@ use App\Classes\Modules\Banks\Services\FetchesBank;
use App\Classes\Modules\Banks\Standards\Rules\CanDeleteBank;
use App\Classes\Modules\Banks\Services\DeletesBank;
use App\Classes\Modules\Banks\Services\CreatesBankLog;
use App\Http\Resources\BankResource;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class DeleteBankLogic extends AbstractControllerLogic
{
@@ -66,20 +67,30 @@ class DeleteBankLogic extends AbstractControllerLogic
*/
public function logic(Request $request) : JsonResponse
{
$this->canDeleteBank->passes();
$bank = $this->fetchesBank->execute(['id' => $request->route('id')]);
$proceed = false;
if($bank->default){
throw new RequestValidationException('You can\'t delete bank account when it set to default');
$isAuthorized = in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES);
if($isAuthorized) {
$banks = $bank->company->banks()->where('default', 1)->get();
if(count($banks) > 1) {
//User should be able to delete themselves, but sometimes there are more than 1 bank set as default (different type, why??!), we need to allow admin to do the delete
$proceed = true;
}
else{
$proceed = false;
}
}
if(!$proceed){
throw new RequestValidationException('You can\'t delete bank account when it set to default');
}
}
$bank = $this->deletesBank->execute($bank);
// $bankLog = $this->createsBankLog->execute($bank);
// $bankLog = $this->createsBankLog->execute($bank);
return $this->response([]);
}
}
}
@@ -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));
}
@@ -86,7 +86,9 @@ class UpdateBookingAmountWithPOLogic extends AbstractControllerLogic
return $product['quantity'] * floatval(str_replace(',', '', $product['unit_price']));
});
$bookingAmountUpdate = (float)$bookingAttribute->value;
$isTally = $total === $bookingAmountUpdate ? true : false;
// $isTally = ($total === $bookingAmountUpdate) ? true : false;
$isTally = bccomp($total, $bookingAmountUpdate, 3) === 0;
if(!$isTally){
throw new MalformedRequestException('Purchase Order total not tally with updated booking amount of ' . $bookingAmountUpdate);
}
@@ -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');
}
}
@@ -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
@@ -2,18 +2,17 @@
namespace App\Classes\Modules\Exports\Services;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Models\Booking;
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 Illuminate\Http\Request;
use App\Classes\Modules\Bookings\Services\CalculatesBookingRefundAmount;
use App\Classes\Modules\Bookings\Services\CalculatesBookingRefundServiceCharge;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
@@ -71,6 +70,8 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR
public function map($booking): array
{
$records = [];
$averageCurrencyRate = 0;
$currencyId = 0;
$purchaseOrder = $booking->transactions()->where('type', TransactionType::PURCHASE_ORDER)->first();
$company = $booking->company()->first();
@@ -79,15 +80,60 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR
if(!$lastPaymentTransaction){
return $records;
}
$documentDate = $lastPaymentTransaction->created_at;
if(Carbon::parse($booking->updated_at)->isAfter($lastPaymentTransaction->created_at)){
$documentDate = $booking->updated_at;
$invoiceTransaction = $booking->transactions()->where('type', TransactionType::INVOICE)->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->first();
$currencyId = $booking->fix_currency_id;
$subtotal = 0;
$displayedSubtotal = 0;
$totalPayment = 0;
$averageCurrencyRate = $invoiceTransaction->currency_rate;
$paymentSum = $booking->transactions()
->where('type', TransactionType::PAYMENT)
->where('status', ApprovalStatus::COMPLETED)
->get()
->sum(function ($transaction) {
return round($transaction->amount, 2);
});
if ($paymentSum){
$averageCurrencyRate = $booking->transactions()
->where('type', TransactionType::PAYMENT)
->where('status', ApprovalStatus::COMPLETED)
->get()
->sum(function ($transaction) {
return $transaction->currency_rate;
}) / $booking->transactions()
->where('type', TransactionType::PAYMENT)
->where('status', ApprovalStatus::COMPLETED)
->count();
$refundedAmount = (App()->make(CalculatesBookingRefundAmount::class))->execute($booking, 1);
$refundedServiceCharge = (App()->make(CalculatesBookingRefundServiceCharge::class))->execute($booking, 1);
$totalPayment = $paymentSum - $refundedAmount - $refundedServiceCharge;
}
$documentDate = $lastPaymentTransaction->created_at;
// if(Carbon::parse($booking->updated_at)->isAfter($lastPaymentTransaction->created_at)){ //cief todo: 90 - Report E-Invoice date incorrect
// $documentDate = $booking->updated_at;
// }
$formattedDocumentDate = Carbon::parse($documentDate)->format('m/d/Y');
$firstItem = true;
$transactionDetails = $purchaseOrder->transactionDetails;
foreach ($transactionDetails as $detail) {
$displayUnitPrice = 0;
if($averageCurrencyRate && $currencyId){
$exactUnitPrice = ($currencyId) === 1 ? $detail->price : bcdiv($detail->price, $averageCurrencyRate, 7);
$displayUnitPrice = round($exactUnitPrice, 2);
$itemTotal = bcmul($exactUnitPrice, $detail->quantity, 5);
$displayedItemTotal = round(bcmul($displayUnitPrice, $detail->quantity, 7), 2);
$displayedSubtotal = bcadd($displayedSubtotal, $displayedItemTotal, 2);
$subtotal = bcadd($subtotal, $itemTotal, 5);
}
$records[] = [
$firstItem ? '<<New>>' : '',
$formattedDocumentDate,
@@ -100,7 +146,7 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR
'022',
'C',
$detail->quantity,
number_format($detail->price, 2),
$displayUnitPrice ? number_format($displayUnitPrice, 2): 0,
$firstItem ? 'T' : '',
$company->e_invoice ? 'F' : 'T'
];
@@ -109,6 +155,87 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR
$firstItem = false;
}
}
// Service Charge - Starts
$serviceCharge = 0;
if (!$totalPayment) {
$serviceCharge = $invoiceTransaction->service_charge;
}
else {
$serviceCharge = $booking->transactions()
->where('type', TransactionType::PAYMENT)
->where('status', ApprovalStatus::COMPLETED)
->get()
->sum(function ($transaction) {
return $transaction->service_charge;
});
}
$records[] = [
'',
$formattedDocumentDate,
$company->debtor,
$booking->marking,
$booking->marking,
'500-0000',
'PRODUCT NAME :',
'Service Charge',
'022',
'C',
'1',
$serviceCharge ? number_format($serviceCharge, 2): '0',
'',
$company->e_invoice ? 'F' : 'T'
];
// Service Charge - Ends
// Adjustment - Starts
$adjustment = 0;
$voucherRedemption = $invoiceTransaction->voucherRedemption;
$voucherDiscount = $voucherRedemption ? bcmul((string)$voucherRedemption->value, "-1", 2) : "0";
$displayedSubtotal = is_numeric($displayedSubtotal) ? sprintf('%F', $displayedSubtotal) : '0';
$serviceCharge = is_numeric($serviceCharge) ? sprintf('%F', $serviceCharge) : '0';
$tax = is_numeric($invoiceTransaction->tax) ? sprintf('%F', $invoiceTransaction->tax) : '0';
$voucherDiscount = is_numeric($voucherDiscount) ? sprintf('%F', $voucherDiscount) : '0';
$displayedTotal = bcadd(
bcadd(
bcadd($displayedSubtotal, $serviceCharge, 5),
$tax,
5
),
$voucherDiscount,
5
);
$expectedTotal = bcadd(bcadd(bcadd($subtotal, $serviceCharge, 5), $invoiceTransaction->tax, 5), $voucherDiscount, 5);
$adjustment = bcsub($expectedTotal, $displayedTotal, 5);
if ($totalPayment) {
$expectedTotal = $totalPayment;
$adjustment = bcsub($expectedTotal, $displayedTotal, 5);
}
$records[] = [
'',
$formattedDocumentDate,
$company->debtor,
$booking->marking,
$booking->marking,
'500-0000',
'PRODUCT NAME :',
'Adjustment',
'022',
'C',
'1',
$adjustment ? number_format($adjustment, 2): '0',
'',
$company->e_invoice ? 'F' : 'T'
];
// Adjustment - Ends
return $records;
}
}
@@ -44,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;
@@ -63,9 +63,9 @@ 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){
@@ -88,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
@@ -151,7 +151,8 @@ class CreateProformaInvoiceTransactionProcessor
$payable_amount = $booking->transactions()->payments()->where(function ($query) {
return $query->where(function ($query) {
return $query->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString());
// return $query->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString());
return $query->where('status', ApprovalStatus::PENDING_SUBMISSION)->where('expires_on', '>=', Carbon::now());
})->orWhere(function ($query) {
return $query->whereIn('status', [ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]);
});
@@ -164,7 +165,8 @@ class CreateProformaInvoiceTransactionProcessor
$paymentAmount = $booking->transactions()->payments()->where(function ($query) {
return $query->where(function ($query) {
return $query->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString());
// return $query->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString());
return $query->where('status', ApprovalStatus::PENDING_SUBMISSION)->where('expires_on', '>=', Carbon::now());
})->orWhere(function ($query) {
return $query->whereIn('status', [ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]);
});
@@ -12,4 +12,29 @@ final class BankAccountType {
public const ALIPAY_RECIPIENT = 4;
/**
* Get all account type labels.
*
* @return array
*/
public static function labels(): array
{
return [
self::PERSONAL => 'PERSONAL',
self::EXTERNAL => 'EXTERNAL',
self::ALIPAY_1688 => 'ALIPAY_1688',
self::ALIPAY_RECIPIENT => 'ALIPAY_RECIPIENT',
];
}
/**
* Get label for a specific account type.
*
* @param int|string $type
* @return string
*/
public static function label($type): string
{
return self::labels()[(int) $type] ?? 'Unknown';
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
+2
View File
@@ -2,6 +2,7 @@
namespace App\Http\Resources;
use App\Classes\ValueObjects\Constants\BankAccountType;
use Illuminate\Http\Resources\Json\JsonResource;
class BankResource extends JsonResource
@@ -27,6 +28,7 @@ class BankResource extends JsonResource
'country_id' => $this->country_id,
'default' => $this->default,
'status' => $this->status,
'bank_account_type_label' => BankAccountType::label($this->type),
];
}
}
@@ -8,6 +8,7 @@
<div class="col">
<h3 class="all-caps">Are you Sure?</h3>
<div class="fs-11">Are you sure you want to delete this bank account?</div>
<!-- <div class="fs-11 text-danger" v-if="$store.getters.isAdmin">Note to Admin: Always make sure that Account Type 'EXTERNAL' has a default set before or after delete</div> -->
</div>
</div>
<div class="row">
@@ -30,4 +31,4 @@
mixins: [componentHandler, ModalFormHandler]
}
</script>
</script>
@@ -415,12 +415,17 @@
<div class="col-2" v-if="showDownloadCreditNote && refund.status === 2">
<div class="row no-margin justify-content-end">
<div class="font-heading all-caps fs-10 m-b-5 text-right">Credit Note</div>
<!-- cief todo: 90 - E Credit Note incomplete-->
<a target=_blank @click="downloadCreditNote(refund.id)" v-if="$store.getters.isSuperAdmin">
<!-- Allow Credit Note to be downloaded for company NOT opted in E-Invoice -->
<a target=_blank @click="downloadCreditNote(refund.id)"
v-if="
refund.booking &&
refund.booking.company &&
!refund.booking.company.e_invoice">
<div class="icon-thumbnail fs-11 text-white icon-25 bg-primary btn-rounded float-left m-r-0 pointer">
<i class="fa fa-file-image-o fs-10"></i>
</div>
</a>
<!-- cief todo: 90 - E Credit Note incomplete (for company opted in E-Invoice)-->
</div>
</div>
</div>
@@ -202,7 +202,19 @@
</div>
<button class="btn btn-xs all-caps b-rad-none btn-default bg-master-lightest w-100" @click="submitted = false" v-if="allowPOEditing">Edit Purchase Order</button>
<!-- <button class="btn btn-xs all-caps b-rad-none btn-default bg-master-lightest w-100" @click="submitted = false" >Edit Purchase Order</button> -->
<button class="btn btn-xs all-caps b-rad-none btn-complete w-100 m-t-5" v-if="!data.documents.proforma_invoice && data.outstanding_amount != 0" @click="submit(route('api.booking.proforma.create', data.id), 'post', section, true, true)">Generate Proforma Invoice</button>
<button class="btn btn-xs all-caps b-rad-none btn-complete w-100 m-t-5" v-if="!data.documents.proforma_invoice && data.outstanding_amount != 0" @click="handleGenerateProformaInvoice">Generate Proforma Invoice</button>
<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>
</div>
</div>
</div>
@@ -263,7 +275,7 @@
//Condition 2
const paymentsMade = Math.round((this.data.paid_amount + Number.EPSILON) * 100) / 100 > 0;
const outstandingAmount = Math.round((this.data.outstanding_amount + Number.EPSILON) * 100) / 100 > 0;
const allPaymentApproved = this.data.payment_history.every(payment => payment.status === 2);
const allPaymentApproved = this.data.payment_history.every(payment => (payment.status === 2 || payment.status === 3));
//Condition 3
const adminBeforeApproval = this.$store.getters.isAdmin && !(this.data.purchase_order.status === 2);
@@ -321,11 +333,26 @@
this.submit(route('api.transaction.po.import', this.data.id), 'post', this.section, true, true);
},
successHandler(){
if((Math.round((this.poTotal + Number.EPSILON) * 1000) / 1000).toFixed(3) === (Math.round((this.data.amount + Number.EPSILON) * 1000) / 1000).toFixed(3)){
this.submitted = true;
successHandler(response, section){
if(section === this.section + 'CheckTransferRule'){
this.checkEInvoiceRule();
}
else if(section === this.section + 'CheckEInvoiceRule'){
if(response.payload.data.isPassed){
this.submit(route('api.booking.proforma.create', this.data.id), 'post', this.section, true, true);
}
}
else{
if((Math.round((this.poTotal + Number.EPSILON) * 1000) / 1000).toFixed(3) === (Math.round((this.data.amount + Number.EPSILON) * 1000) / 1000).toFixed(3)){
this.submitted = true;
}
this.updateList()
}
},
errorHandler(error, statusCode, section) { //E-Invoice
if(section === this.section + 'CheckEInvoiceRule' && statusCode === 422){
$('#modal-einvoice-info').modal('show');
}
this.updateList()
},
addProduct(){
this.products.push({
@@ -354,6 +381,31 @@
},
removeProduct(index){
this.products.splice(index, 1);
},
handleGenerateProformaInvoice(){
this.checkTransferRule();
},
checkTransferRule(){
this.error = '';
this.parameters = {
booking_id: this.data.id,
company_id: this.data.company.id,
};
this.submit(route('api.rule.check.transfer'), 'post', this.section + 'CheckTransferRule', false, true);
},
checkEInvoiceRule(){
this.error = '';
this.parameters = {
company_id: this.data.company.id,
};
this.submit(route('api.rule.check.einvoice'), 'post', this.section + 'CheckEInvoiceRule', false, true);
},
updatedEInvoiceInfo(info){
this.$store.dispatch('reloadList', {'name': "bookingDetailSection"});
},
changeOfMindEInvoiceRequest(){
this.parameters.e_invoice_request = false;
this.submit((this.route('api.company.einvoice.request.change')), 'post', this.section + 'ChangeOfMind', true, true);
}
},
mixins: [formHandler]
@@ -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;
}
@@ -24,6 +24,10 @@
<div class="font-heading fs-10">{{item.holder_name}}</div>
<div class="font-heading fs-11 text-complete m-b-5">{{item.type === 3 ? item.account_no : item.account_no.replace(/[^\dA-Z]/g, '').replace(/(.{4})/g, '$1 ').trim() }}</div>
</div>
<div class="col">
<div class="font-heading fs-10">Account Type</div>
<div class="font-heading fs-11">{{item.bank_account_type_label}}</div>
</div>
<div class="col-auto b-l b-grey">
<div class="row h-100">
<div class="col">
@@ -37,7 +41,7 @@
</div>
</a>
</div>
<div class="col-auto m-l-10 d-flex align-items-center">
<!-- <div class="col-auto m-l-10 d-flex align-items-center">
<div class="row align-items-center">
<div class="col-auto text-right p-t-5 p-b-5" :class="[{'bg-warning-lighter': item.default}, {'bg-master-light': !item.default}]">
<div data-toggle="tooltip" class="row align-items-center justify-content-center" :class="{'link': !item.default}">
@@ -50,6 +54,17 @@
<small class="fs-12 all-caps bold" :class="[{'text-warning': item.default}, {'text-master': !item.default}]">Default Bank</small>
</div>
</div>
</div> -->
<div class="col-auto m-l-10 d-flex align-items-center requestModal" data-type="deleteBankModal">
<div data-toggle="tooltip" title="" data-placement="bottom" class="row link align-items-center justify-content-center" data-original-title="Edit">
<div class="btn btn-xs btn-outline-danger b-rad-none m-r-5 requestModalol">
<i class="fa fa-times"></i>
</div>
</div>
</div>
<div class="col-auto m-l-10 d-flex align-items-center">
<div class="btn btn-xs btn-outline-success b-rad-none pointer requestModal" data-type="defaultBankModal" v-if="item.company_business_type !== 1 && !item.default">Set As Default</div>
<div class="bg-master-lighter p-t-10 p-b-10 p-r-15 p-l-15 muted hint-text fs-10" v-if="item.company_business_type !== 1 && item.default">Set As Default</div>
</div>
</div>
</div>
@@ -58,6 +73,12 @@
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="editBankModal">
<edit-bank-form-component :data="item" :section="section" :company_id="company_id"></edit-bank-form-component>
</modal-component>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="deleteBankModal">
<delete-bank-account-form-component :data="item" :section="section" class="text-center"></delete-bank-account-form-component>
</modal-component>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="defaultBankModal">
<set-bank-account-default-form-component :data="item" :section="section" class="text-center"></set-bank-account-default-form-component>
</modal-component>
</div>
</template>
@@ -75,4 +96,4 @@
},
mixins: [componentHandler]
}
</script>
</script>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="container-fluid">
<div class="row no-margin">
<div class="col p-l-0 p-t-20 p-b-20 sm-text-center">
<small class="small no-margin pull-left sm-pull-reset all-caps fs-10 muted" style=" letter-spacing: 1px; ">Copyright © {{ date('Y') }} CIEF Exchange. All rights reserved. Powered by Laravel Vapor.</small>
<small class="small no-margin pull-left sm-pull-reset all-caps fs-10 muted" style=" letter-spacing: 1px; ">Copyright © {{ date('Y') }} CIEF Exchange. All rights reserved.</small>
</div>
</div>
</div>
+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);
});