20231109 Meeting Feedback 1 Fixes for warehouse storage charges

This commit is contained in:
Dillon Ngo
2023-11-16 22:40:26 +08:00
parent ce94dd1393
commit 76e3668563
31 changed files with 1105 additions and 148 deletions
@@ -0,0 +1,20 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class OrderByIdDesc implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->orderBy('id', 'desc');
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class OrderByUpdatedAtDesc implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->orderBy('updated_at', 'desc');
}
}
@@ -18,6 +18,7 @@ use App\Classes\Modules\Billplzs\Processors\CallbackBillplzProcessor;
use App\Classes\Modules\Transactions\Services\FetchesTransaction;
use App\Classes\Modules\Transactions\Processors\CreatePaymentTransactionProcessor;
use App\Classes\Modules\Transactions\Services\UpdatesTransactionStatus;
use App\Classes\Modules\Transactions\Processors\CheckAndCreateStorageInvoiceTransactionProcessor;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\Group;
@@ -51,6 +52,9 @@ class CallbackBillplzLogic
/** @var CallbackBillplzProcessor */
private $callbackBillplzProcessor;
/** @var CheckAndCreateStorageInvoiceTransactionProcessor */
private $storageInvoiceTransactionProcessor;
/**
* CallbackBillplzLogic constructor.
* @param GetBillplzBill $getBillplzBill
@@ -60,8 +64,9 @@ class CallbackBillplzLogic
* @param UpdateDoFromYDPortalProcessor $updateDoFromYDPortalProcessor
* @param CreatePaymentTransactionProcessor $createPaymentTransactionProcessor
* @param CallbackBillplzProcessor $callbackBillplzProcessor
* @param CheckAndCreateStorageInvoiceTransactionProcessor $storageInvoiceTransactionProcessor
*/
public function __construct(GetBillplzBill $getBillplzBill, FetchesTransaction $fetchesTransaction, UpdatesTransactionStatus $updatesTransactionStatus, UpdateDoFromVTPortalProcessor $updateDoFromVTPortalProcessor, UpdateDoFromYDPortalProcessor $updateDoFromYDPortalProcessor, UpdatesWalletBalance $updatesWalletBalance, CreatePaymentTransactionProcessor $createPaymentTransactionProcessor, CallbackBillplzProcessor $callbackBillplzProcessor)
public function __construct(GetBillplzBill $getBillplzBill, FetchesTransaction $fetchesTransaction, UpdatesTransactionStatus $updatesTransactionStatus, UpdateDoFromVTPortalProcessor $updateDoFromVTPortalProcessor, UpdateDoFromYDPortalProcessor $updateDoFromYDPortalProcessor, UpdatesWalletBalance $updatesWalletBalance, CreatePaymentTransactionProcessor $createPaymentTransactionProcessor, CallbackBillplzProcessor $callbackBillplzProcessor, CheckAndCreateStorageInvoiceTransactionProcessor $storageInvoiceTransactionProcessor)
{
$this->getBillplzBill = $getBillplzBill;
$this->fetchesTransaction = $fetchesTransaction;
@@ -71,6 +76,7 @@ class CallbackBillplzLogic
$this->updatesWalletBalance = $updatesWalletBalance;
$this->createPaymentTransactionProcessor = $createPaymentTransactionProcessor;
$this->callbackBillplzProcessor = $callbackBillplzProcessor;
$this->storageInvoiceTransactionProcessor = $storageInvoiceTransactionProcessor;
}
@@ -115,7 +121,8 @@ class CallbackBillplzLogic
$token = Auth::fromUser(User::find(1));
$request->headers->set('Authorization', 'Bearer '.$token);
$this->callbackBillplzProcessor->execute($transaction, $status);
$this->storageInvoiceBackDoorPreventionCheck($transaction, $status);
$result = $this->callbackBillplzProcessor->execute($transaction, $status);
$company_module_marking = $transaction->owner->owner->connections? $transaction->owner->owner->connections->first()->invitee_reference: null;
@@ -126,6 +133,22 @@ class CallbackBillplzLogic
$company_module_marking = $order->companyModule->connections? $order->companyModule->connections->first()->invitee_reference: null;
}
return $request->method() === 'POST' ? true : view('pages.payments_redirect', ['marking' => $order->reference ?? null, 'company_module_marking' => $company_module_marking ?? null, 'transaction' => $transaction, 'status' => $status]);
return $request->method() === 'POST' ? true : view('pages.payments_redirect', ['marking' => $order->reference ?? null, 'company_module_marking' => $company_module_marking ?? null, 'transaction' => $transaction, 'status' => $status, 'result' => $result]);
}
private function storageInvoiceBackDoorPreventionCheck($transaction, $status){
if ($transaction->owner instanceof Wallet && $status === ApprovalStatus::APPROVED) {
$group = Group::where('reference', $transaction->payment_reference)->first();
if ($group) {
foreach ($group->groupTransactions as $groupTransaction) {
$invoice = $groupTransaction->transaction;
$pL = $invoice->owner;
$order = $pL->owner;
if($order){
$this->storageInvoiceTransactionProcessor->executeOrder($order, false); //original was set true here so that no group is soft deleted or billplz bill got deleted
}
}
}
}
}
}
@@ -65,6 +65,9 @@ class CallbackBillplzProcessor
$invoice = $transaction->owner;
$packingList = $invoice->owner;
$proceed = $this->checkAmountPaidVSRequired($transaction);
if(!$proceed) return false;
$this->updatesTransactionStatus->execute($transaction, $status);
// check if is wallet top up
@@ -95,13 +98,39 @@ class CallbackBillplzProcessor
if (!$transaction->owner instanceof Wallet) {
$this->processPackingListAndInvoice($packingList, $invoice);
}
return true;
}
private function checkAmountPaidVSRequired($transaction){
//command:check-storage-invoices must already run for this part of the code to work properly
$group = Group::withTrashed()->where('reference', $transaction->payment_reference)->first();
if ($group) {
$totalAmountToBePaid = 0;
$actualAmountPaid = $transaction->amount;
foreach ($group->groupTransactions as $groupTransaction) {
$invoice = $groupTransaction->transaction;
$totalAmountToBePaid += $invoice->amount;
}
if(($totalAmountToBePaid - $actualAmountPaid) < 0.01){
}
else{
Log::channel('storage_invoices')->info('Total amount from current transaction: '.$totalAmountToBePaid); //cief todo: to be removed
Log::channel('storage_invoices')->info('Total amount from paid transaction: '.$transaction->amount); //cief todo: to be removed
return false;
}
}
return true;
}
private function processPackingListAndInvoice($packingList, $invoice = null){
$result = false;
$totalInvoicesAmountPaid = 0.00;
$totalInvoicesAmount = 0.00;
$invoiceTransactions = $packingList->transactions()->whereIn('type', [TransactionType::SHIPPING_INVOICE, TransactionType::STORAGE_INVOICE])->get();
$invoiceTransactions = $packingList->transactions()->where('status', [ApprovalStatus::APPROVED])->whereIn('type', [TransactionType::SHIPPING_INVOICE, TransactionType::STORAGE_INVOICE])->get();
//Part 1: Process each single invoice type and get the total of all invoices
foreach($invoiceTransactions as $invoiceTransaction){
@@ -121,12 +150,12 @@ class CallbackBillplzProcessor
$totalInvoicesAmountPaid = $totalInvoicesAmountPaid + $totalInvoiceAmountPaid;
}
Log::info('Total invoice amount paid 1: '.$totalInvoicesAmount); //cief todo: to be removed
Log::info('Total invoice amount paid 2: '.$totalInvoicesAmountPaid); //cief todo: to be removed
Log::channel('storage_invoices')->info('Total invoice amount paid 1: '.$totalInvoicesAmount); //cief todo: to be removed
Log::channel('storage_invoices')->info('Total invoice amount paid 2: '.$totalInvoicesAmountPaid); //cief todo: to be removed
//Part 2: Based on the collected info for all the total of all invoices
if (($totalInvoicesAmount - $totalInvoicesAmountPaid) < 0.01) {
Log::info('Total invoice amount paid 3: '.$totalInvoicesAmountPaid); //cief todo: to be removed
Log::channel('storage_invoices')->info('Total invoice amount paid 3: '.$totalInvoicesAmountPaid); //cief todo: to be removed
$packingList->status = ApprovalStatus::APPROVED;
$packingList->save();
@@ -0,0 +1,34 @@
<?php
namespace App\Classes\Modules\Billplzs\Services;
use Illuminate\Support\Facades\Http;
use App\Classes\Exceptions\MalformedRequestException;
use Illuminate\Support\Facades\Log;
class DeletesBillplzBill
{
/**
* @param string $billID
* @throws MalformedRequestException
*/
public function execute(string $billID) {
try{
$response = Http::withBasicAuth(config('billplz.api_key').':', '')->delete(config('billplz.base_url').'/api/v3/bills/'.$billID);
Log::channel('storage_invoices')->info('DeletesBillplzBill response: '.json_encode($response));
if($response->successful()){
$data = $response->json();
// $data['url'] = $data['url'].'?auto_submit=true';
return (object) $data;
}else{
return null;
}
}catch(\Exception $exception){
throw new MalformedRequestException('Unable to get correct response from billplz server: ' . $exception->getMessage());
}
}
}
@@ -52,8 +52,10 @@ class FetchOrderV2Logic extends AbstractControllerLogic
$this->canFetchOrder->passes();
$query = $this->fetchesOrder->execute(['reference' => $request->route('id'), 'with_packing_lists' => true]);
$query->storages = $request->input('storages'); //from middleware
if($request->input('storages')){
$query->storages = $request->input('storages'); //from middleware
}
return $this->resourceResponse(new OrderV2Resource($query));
@@ -121,7 +121,7 @@ class FetchContainersFromYdPortalProcessor
$containerReference = explode('预计到港时间', $tracking[1])[0];
$loadingDate = Carbon::parse($trackingRow->trackingtime);
$etd = Carbon::parse($tracking[2])->subDays(5);
$eta = Carbon::parse($tracking[2])->addDays(2);
// $eta = Carbon::parse($tracking[2])->addDays(2); //omair to review
}
}
@@ -147,7 +147,7 @@ class FetchContainersUpdatesFromYdPortalProcessor
if($delayDate){
$delayDate = $delayDate->addDays(2);
// $delayDate = $delayDate->addDays(2); //omair to review
$transport = $container->transports()->first();
if(!$transport->schedules()->whereDate('eta', '>=', $delayDate)->first()) {
@@ -5,10 +5,11 @@ namespace App\Classes\Modules\Transactions\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Transactions\Services\ListsTransactions;
use App\Http\Resources\BookingResource;
use App\Http\Resources\TransactionResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class ListTransactionsLogic extends AbstractControllerLogic
{
@@ -38,13 +39,18 @@ class ListTransactionsLogic extends AbstractControllerLogic
public function logic(Request $request) : JsonResponse
{
$query = $this->listsTransactions->execute($this->listsTransactions->deserializeFilters($request->input('filters')));
if($request->input('storages')){
foreach ($query->items() as &$item) {
$transactionId = $item['id'];
$filteredStorages = array_filter($request->input('storages'), function ($storage) use ($transactionId) {
return isset($storage['parentInvoiceId']) && $storage['parentInvoiceId'] == $transactionId;
});
$item['storages'] = $filteredStorages;
}
}
return $this->collectionResponse(TransactionResource::collection($query));
}
}
@@ -13,6 +13,7 @@ use App\Classes\Modules\Transactions\Services\FetchesTransaction;
use App\Classes\Modules\Transactions\Services\UpdatesTransactionDetail;
use App\Classes\Modules\Transactions\Services\UpdatesTransactionStatus;
use App\Classes\Modules\Transactions\Services\DeletesGroup;
use App\Classes\Modules\Billplzs\Services\DeletesBillplzBill;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionDetailObject;
@@ -22,7 +23,8 @@ use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PackageType;
use App\Classes\ValueObjects\Constants\TransactionDetailType;
use App\Http\Resources\TransactionResource;
use App\Models\Order;
use App\Models\PackingList;
use App\Models\Transaction;
use Carbon\Carbon;
@@ -58,6 +60,9 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
/** @var DeletesGroup */
private $deletesGroup;
/** @var DeletesBillplzBill */
private $deletesBillplzBill;
/**
* @param FetchesOrder $fetchesOrder
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
@@ -68,8 +73,9 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
* @param UpdatesTransactionDetail $updatesTransactionDetail
* @param UpdatesTransactionStatus $updatesTransactionStatus
* @param DeletesGroup $deletesGroup
* @param DeletesBillplzBill $deletesBillplzBill
*/
public function __construct(FetchesOrder $fetchesOrder, GeneratesTransactionBillNumber $generatesTransactionBillNumber, FetchesTransaction $fetchesTransaction, CreatesTransaction $createsTransaction, CreatesTransactionDetail $createsTransactionDetail, UpdatesTransaction $updatesTransaction, UpdatesTransactionDetail $updatesTransactionDetail, UpdatesTransactionStatus $updatesTransactionStatus, DeletesGroup $deletesGroup)
public function __construct(FetchesOrder $fetchesOrder, GeneratesTransactionBillNumber $generatesTransactionBillNumber, FetchesTransaction $fetchesTransaction, CreatesTransaction $createsTransaction, CreatesTransactionDetail $createsTransactionDetail, UpdatesTransaction $updatesTransaction, UpdatesTransactionDetail $updatesTransactionDetail, UpdatesTransactionStatus $updatesTransactionStatus, DeletesGroup $deletesGroup, DeletesBillplzBill $deletesBillplzBill)
{
$this->fetchesOrder = $fetchesOrder;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
@@ -80,50 +86,72 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
$this->updatesTransactionDetail = $updatesTransactionDetail;
$this->updatesTransactionStatus = $updatesTransactionStatus;
$this->deletesGroup = $deletesGroup;
$this->deletesBillplzBill = $deletesBillplzBill;
}
/**
* @throws MalformedRequestException
*/
public function execute(int $orderId)
public function execute(int $orderReference)
{
$multipleResults = array();
$order = $this->fetchesOrder->execute(['reference' => $orderId, 'with_packing_lists' => true]);
$order = $this->fetchesOrder->execute(['reference' => $orderReference, 'with_packing_lists' => true]);
return $this->executeOrder($order);
}
$eta = "";
$destinationWarehousePackages = $order->destinationWarehousePackages;
foreach ($destinationWarehousePackages as $destinationWarehousePackage){
if ($destinationWarehousePackage) {
$package = $destinationWarehousePackage->packages->first();
public function executeOrder(Order $order, bool $isBackDoorCheck = false){
$results = [];
$marking = $order->companyModule->inviters()->withPivot('invitee_reference')->first()->pivot->invitee_reference;
$is_credit_term = $order->companyModule->inviters()->withPivot('is_credit_term')->first()->pivot->is_credit_term;
if(!$is_credit_term){
Log::channel('storage_invoices')->info('orderId: '.$order->id.', orderReference: '.$order->reference.', marking: '.$marking.', is_credit_term: '.$is_credit_term);
$packingLists = $order->destinationWarehousePackages;
foreach ($packingLists as $packingList){
Log::channel('storage_invoices')->info('destinationWarehousePackage: '.json_encode($packingList));
$eta = $this->getEtaFromPackingList($packingList);
if($eta){
$transactions = $packingList->transactions()->where('transactions.type', TransactionType::SHIPPING_INVOICE)->whereIn('transactions.status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->get();
// $transactions = $destinationWarehousePackage->transactions()->where('transactions.type', TransactionType::SHIPPING_INVOICE)->where('transactions.status', ApprovalStatus::APPROVED)->get();
/** @var Transaction $invoice_transaction */
foreach ($transactions as $invoice_transaction){
$result = $this->processSingleTransactionOfTypeShippingInvoice($invoice_transaction, $packingList, $order->company_module_id, $eta, $isBackDoorCheck);
if($result){
$results[] = $result;
}
}
}
}
}
return $results;
}
private function getEtaFromPackingList($packingList){
if ($packingList) {
$package = $packingList->packages->first();
if ($package) {
$container = $package->container()->first();
if ($container) {
$transport = $container->transports->first();
if ($transport) {
// $container->transports()->first()->update(['drop_date' => $unstuffingDate, 'status' => ApprovalStatus::COMPLETED]);
// $arrivalDateAtChinaWarehouse = Carbon::parse($transport->drop_date); //cief todo: to uncomment this
$arrivalDateAtChinaWarehouse = Carbon::parse('2021-11-13 00:00:00');
$dateToCompare = Carbon::parse('2023-11-13 00:00:00');
if ($dateToCompare->isAfter($arrivalDateAtChinaWarehouse)) {
Log::channel('storage_invoices')->info('dateToCompare: '.$dateToCompare.', arrivalDateAtChinaWarehouse: '.$arrivalDateAtChinaWarehouse);
$schedule = $transport->schedules->last();
if ($schedule) {
$eta = $schedule->eta;
if($eta){
$transactions = $destinationWarehousePackage->transactions()->where('transactions.type', TransactionType::SHIPPING_INVOICE)->whereIn('transactions.status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->get();
// $transactions = $destinationWarehousePackage->transactions()->where('transactions.type', TransactionType::SHIPPING_INVOICE)->where('transactions.status', ApprovalStatus::APPROVED)->get();
/** @var Transaction $invoice_transaction */
foreach ($transactions as $invoice_transaction){
$multipleResults[] = $this->processSingleTransactionOfTypeShippingInvoice($invoice_transaction, $destinationWarehousePackage, $order, $eta);
}
return $schedule->eta;
}
}
}
}
}
}
return null;
}
return $multipleResults;
}
function processSingleTransactionOfTypeShippingInvoice($transaction, $destinationWarehousePackage, $order, $eta){
private function processSingleTransactionOfTypeShippingInvoice($transaction, $destinationWarehousePackage, $company_module_id, $eta, $isBackDoorCheck){
$pricePerCBM = 3;
$resultNumberOfDaysFree = 10;
$dt1 = $eta->copy()->addDay()->startOfDay();
@@ -131,7 +159,16 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
$currentDatetime = Carbon::now();
$dt2 = $currentDatetime->copy()->addDay()->startOfDay();
$resultCurrentDate = $dt2->format('Y-m-d H:i:s');
$test = Carbon::parse($dt2);
$interval = Carbon::parse($dt2)->diff($dt1);
$interval2 = $dt2->diff($dt1);
//cief todo: to be deleted
// Log::channel('storage_invoices')->info('currentDatetime: '.$currentDatetime);
// Log::channel('storage_invoices')->info('dt1: '.$dt1.', resultStartDate: '.$resultStartDate);
// Log::channel('storage_invoices')->info('dt2: '.$dt2.', resultCurrentDate: '.$resultCurrentDate);
// Log::channel('storage_invoices')->info('interval: '.$interval->days.', test: '.$test.', interval2: '.$interval2->days);
$resultNumberOfDaysExceeded = $interval->days - $resultNumberOfDaysFree;
$storageInvoice = $destinationWarehousePackage->transactions()->where('transactions.type', TransactionType::STORAGE_INVOICE)->first();
@@ -150,52 +187,38 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
if(!$storageInvoice && $resultNumberOfDaysExceeded > 0 && $transaction->status != ApprovalStatus::COMPLETED){
$billNumber = $this->generatesTransactionBillNumber->execute('STOR-');
$invoiceTransaction = $this->createTransaction($destinationWarehousePackage, $billNumber, $order->company_module_id, $price_cbm);
$storageInvoiceId = $invoiceTransaction->id;
$this->createTransactionDetails($invoiceTransaction, $destinationWarehousePackage, $cbm, 3 * $resultNumberOfDaysExceeded, $resultNumberOfDaysExceeded);
$storageInvoice = $this->createStorageInvoiceTransaction($destinationWarehousePackage, $billNumber, $company_module_id, $price_cbm);
$storageInvoiceId = $storageInvoice->id;
$this->createStorageInvoiceTransactionDetails($storageInvoice, $destinationWarehousePackage, $cbm, 3 * $resultNumberOfDaysExceeded, $resultNumberOfDaysExceeded);
}
else if($storageInvoice){
$amount = $storageInvoice->amount;
$epsilon = 0.0001; // Tolerance for the comparison
//cief todo: to be removed - starts
Log::info('price_cbm: '.$price_cbm."-".gettype($price_cbm));
Log::info('amount: '.$amount."-".gettype($amount));
if (abs($price_cbm - $amount) > $epsilon) {
Log::info("The values are not equal.");
} else {
Log::info("The values are approximately equal.");
}
//cief todo: to be removed - ends
Log::channel('storage_invoices')->info('$transaction->id,: '.$transaction->id);
Log::channel('storage_invoices')->info('$storageInvoice->status,: '.$storageInvoice->status);
Log::channel('storage_invoices')->info('price_cbm: '.$price_cbm."-".gettype($price_cbm));
if(abs($price_cbm - $amount) > $epsilon && $storageInvoice->status !== ApprovalStatus::COMPLETED){
$paymentTransactions = $storageInvoice->transactions()->where('transactions.type', TransactionType::PAYMENT)->where('transactions.status', ApprovalStatus::PENDING_SUBMISSION)->get();
if(count($paymentTransactions) > 0){ //PAYMENT type
foreach ($paymentTransactions as $paymentTransaction){
$this->updatesTransactionStatus->execute($paymentTransaction, ApprovalStatus::EXPIRED);
}
}
else{ //TOP UP type
$groups = $storageInvoice->groups()->get();
foreach ($groups as $grp){
$groupReference = $grp->reference;
$walletTransaction = $this->fetchesTransaction->execute(['payment_reference' => $groupReference]);
if($walletTransaction->status === ApprovalStatus::PENDING_SUBMISSION || $walletTransaction->status === ApprovalStatus::PENDING_VERIFICATION){
$grp->status = ApprovalStatus::EXPIRED;
$grp->save();
$this->deletesGroup->execute($grp);
$this->updatesTransactionStatus->execute($walletTransaction, ApprovalStatus::EXPIRED);
if(!$isBackDoorCheck){
if(count($paymentTransactions) > 0){
$this->updatePaymentTransactionViaNonGroupPayment($paymentTransactions);
}
else{
$this->updatePaymentTransactionViaGroupPayment($storageInvoice);
}
}
$invoiceTransaction = $this->updateTransaction($storageInvoice, $price_cbm);
$storageInvoice = $this->updateStorageInvoiceTransaction($storageInvoice, $price_cbm);
$invoiceTransactionDetails = $storageInvoice->transactionDetails()->first();
$this->updateTransactionDetails($invoiceTransactionDetails, $cbm, 3 * $resultNumberOfDaysExceeded);
$this->updateStorageInvoiceTransactionDetails($invoiceTransactionDetails, $cbm, 3 * $resultNumberOfDaysExceeded);
}
$storageInvoiceId = $storageInvoice->id;
}
if($storageInvoiceId !== 0){
$result = [
'parentInvoiceId' => $transaction->id,
'storageInvoiceId' => $storageInvoiceId,
@@ -205,12 +228,44 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
'currentDate' => $resultCurrentDate,
'cbm' => $cbm,
'pricePerCBM' => $pricePerCBM,
'storageInvoice' => new TransactionResource($storageInvoice)
];
return $result;
}
function updateTransaction(Transaction $transaction, float $totalAmount){
}
private function updatePaymentTransactionViaGroupPayment($storageInvoice){
Log::channel('storage_invoices')->info('updatePaymentTransactionViaGroupPayment');
$groups = $storageInvoice->groups()->get();
foreach ($groups as $grp){
$groupReference = $grp->reference;
$walletTransaction = $this->fetchesTransaction->execute(['payment_reference' => $groupReference]);
if($walletTransaction->status === ApprovalStatus::PENDING_SUBMISSION || $walletTransaction->status === ApprovalStatus::PENDING_VERIFICATION){
$grp->status = ApprovalStatus::EXPIRED;
$grp->save();
$this->deletesGroup->execute($grp);
$this->updatesTransactionStatus->execute($walletTransaction, ApprovalStatus::EXPIRED);
if($walletTransaction->payment_reference){
$deletedBillplzBill = $this->deletesBillplzBill->execute($walletTransaction->payment_reference);
Log::channel('storage_invoices')->info('deletedBillplzBill TransactionType::WALLET: '.json_encode($deletedBillplzBill));
}
}
}
}
private function updatePaymentTransactionViaNonGroupPayment($paymentTransactions){
Log::channel('storage_invoices')->info('updatePaymentTransactionViaNonGroupPayment');
foreach ($paymentTransactions as $paymentTransaction){
$this->updatesTransactionStatus->execute($paymentTransaction, ApprovalStatus::EXPIRED);
if($paymentTransaction->payment_reference){
$deletedBillplzBill = $this->deletesBillplzBill->execute($paymentTransaction->payment_reference);
Log::channel('storage_invoices')->info('deletedBillplzBill TransactionType::WALLET: '.json_encode($deletedBillplzBill));
}
}
}
private function updateStorageInvoiceTransaction(Transaction $transaction, float $totalAmount){
$object = new TransactionObject(
$transaction->bill_no,
@@ -236,7 +291,7 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
return $invoice_transaction;
}
function updateTransactionDetails($invoice_transaction_details, $cbm, $price_cbm){
private function updateStorageInvoiceTransactionDetails($invoice_transaction_details, $cbm, $price_cbm){
$object_detail = new TransactionDetailObject(
'STORAGE_FEE',
$invoice_transaction_details->name,
@@ -247,7 +302,7 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
$this->updatesTransactionDetail->execute($invoice_transaction_details, $object_detail);
}
function createTransaction(PackingList $packing_list, string $billNumber, int $companyModuleId, float $totalAmount){
private function createStorageInvoiceTransaction(PackingList $packing_list, string $billNumber, int $companyModuleId, float $totalAmount){
$object = new TransactionObject(
$billNumber,
@@ -273,7 +328,7 @@ class CheckAndCreateStorageInvoiceTransactionProcessor
return $invoice_transaction;
}
function createTransactionDetails($invoice_transaction, PackingList $packing_list, $cbm, $price_cbm, $numberOfDays){
private function createStorageInvoiceTransactionDetails($invoice_transaction, PackingList $packing_list, $cbm, $price_cbm, $numberOfDays){
$object_detail = new TransactionDetailObject(
'SHIPPING_FEE',
TransactionDetailType::STORAGE_FEE.' for '.$numberOfDays. ' days<br>'.round($packing_list->packages->where('type', '!=', PackageType::OVER_WEIGHT)->sum('quantity'), 3).' CTNS - '.round($cbm, 3).' CBM',
@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands;
use App\Classes\Modules\Transactions\Services\ListsGroups;
use App\Classes\Modules\Transactions\Processors\CheckAndCreateStorageInvoiceTransactionProcessor;
use App\Models\Order;
use Carbon\Carbon;
use Illuminate\Console\Command;
class CheckStorageInvoices extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:check-storage-invoices';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check all storage invoices due to make sure that they are up to date daily, prevent \'back door\' cases';
/** @var ListsGroups */
private $listsGroups;
/** @var CheckAndCreateStorageInvoiceTransactionProcessor */
private $storageInvoiceTransactionProcessor;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(ListsGroups $listsGroups, CheckAndCreateStorageInvoiceTransactionProcessor $storageInvoiceTransactionProcessor)
{
parent::__construct();
$this->listsGroups = $listsGroups;
$this->storageInvoiceTransactionProcessor = $storageInvoiceTransactionProcessor;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
ini_set('memory_limit', '-1');
$this->info(Carbon::now() . ': Start Check all pending group payment with storage invoice is valid.');
$start = new Carbon();
$newfilters['order_by_updated_at_desc'] = true;
$newfilters['status_in'] = [0, 1];
$groups = $this->listsGroups->execute($newfilters);
foreach ($groups as $group){
$this->info('CheckForStorageInvoiceByTransactions group: '.json_encode($group));
foreach ($group->groupTransactions as $groupTransaction) {
$invoice = $groupTransaction->transaction;
$packingList = $invoice->owner()->first();
if($packingList){
$order = $packingList->owner()->first();
if($order instanceof Order){
$storages = $this->storageInvoiceTransactionProcessor->executeOrder($order);
}
}
}
}
$end = new Carbon();
$elapsedTime = $start->diff($end)->format('%H:%I:%S');
$this->info(Carbon::now() . ': Done Check all pending group payment with storage invoice is valid. ElapsedTime: ' . $elapsedTime . '.');
}
}
+5
View File
@@ -62,6 +62,11 @@ class Kernel extends ConsoleKernel
->hourly()
->withoutOverlapping()
->appendOutputTo (storage_path().'/logs/fix_failed_callback_from_billplz.log');
$schedule->command('command:check-storage-invoices')
->dailyAt('0:01')
->withoutOverlapping()
->appendOutputTo(storage_path().'/logs/check_storage_invoices.log');
}
/**
+1
View File
@@ -72,5 +72,6 @@ class Kernel extends HttpKernel
'token.check' => \App\Http\Middleware\TokenCheckerMiddleware::class,
'storage.invoice.check.byorder' => \App\Http\Middleware\CheckForStorageInvoiceByOrderId::class,
'storage.invoice.check.bytransactions' => \App\Http\Middleware\CheckForStorageInvoiceByTransactions::class,
'storage.invoice.check.bygroup' => \App\Http\Middleware\CheckForStorageInvoiceByGroup::class,
];
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Middleware;
use Closure;
use App\Classes\Modules\Transactions\Processors\CheckAndCreateStorageInvoiceTransactionProcessor;
use App\Classes\Modules\Transactions\Services\ListsGroups;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class CheckForStorageInvoiceByGroup
{
/** @var CheckAndCreateStorageInvoiceTransactionProcessor */
private $storageInvoiceTransactionProcessor;
/** @var ListsGroups */
private $listsGroups;
public function __construct(CheckAndCreateStorageInvoiceTransactionProcessor $storageInvoiceTransactionProcessor, ListsGroups $listsGroups)
{
$this->storageInvoiceTransactionProcessor = $storageInvoiceTransactionProcessor;
$this->listsGroups = $listsGroups;
}
/**
* Handle an incoming request.
*
* @param Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
// :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [0, 1], 'receiver': company_module_id, order_by: {column: 'updated_at', DESC: true}}"
$filters = json_decode($request->input('filters'), true);
if(json_encode($filters['order_by']) == '{"column":"updated_at","DESC":true}'){
$filters['order_by_updated_at_desc'] = true;
}
unset($filters['order_by']);
$groups = $this->listsGroups->execute($filters);
if(isset($filters['check_for_storage_invoice'])){
foreach ($groups as $group){
foreach ($group->groupTransactions as $groupTransaction) {
$invoice = $groupTransaction->transaction;
$packingList = $invoice->owner()->first();
if($packingList){
$order = $packingList->owner()->first();
if($order instanceof Order){
$storages = $this->storageInvoiceTransactionProcessor->executeOrder($order);
}
}
}
}
}
return $next($request);
}
}
@@ -29,9 +29,8 @@ class CheckForStorageInvoiceByOrderId
public function handle(Request $request, Closure $next)
{
$orderId = $request->route('id');
$result = $this->storageInvoiceTransactionProcessor->execute($orderId);
$request->merge(['storages' => $result]);
$storages = $this->storageInvoiceTransactionProcessor->execute($orderId);
$request->merge(['storages' => $storages]);
return $next($request);
}
}
@@ -5,10 +5,12 @@ namespace App\Http\Middleware;
use Closure;
use App\Classes\Modules\Transactions\Processors\CheckAndCreateStorageInvoiceTransactionProcessor;
use App\Classes\Modules\Transactions\Services\ListsTransactions;
use App\Classes\Modules\Transactions\Services\ListsGroups;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\CompanyConnection;
use App\Models\Order;
use App\Models\PackingList;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class CheckForStorageInvoiceByTransactions
{
@@ -19,11 +21,15 @@ class CheckForStorageInvoiceByTransactions
/** @var ListsTransactions */
private $listsTransactions;
/** @var ListsGroups */
private $listsGroups;
public function __construct(CheckAndCreateStorageInvoiceTransactionProcessor $storageInvoiceTransactionProcessor, ListsTransactions $listsTransactions)
public function __construct(CheckAndCreateStorageInvoiceTransactionProcessor $storageInvoiceTransactionProcessor, ListsTransactions $listsTransactions, ListsGroups $listsGroups)
{
$this->storageInvoiceTransactionProcessor = $storageInvoiceTransactionProcessor;
$this->listsTransactions = $listsTransactions;
$this->listsGroups = $listsGroups;
}
@@ -36,36 +42,55 @@ class CheckForStorageInvoiceByTransactions
*/
public function handle(Request $request, Closure $next)
{
//{"per_page":999,"order_by":{"column":"id","DESC":true},"status_in":[2],"receiver":298,"type_in":[1,16],"does_not_have_payment_status_in":[0,1],"does_not_have_groups":1}
$results = [];
$transactions = null;
$marking = $request->route('marking');
if($marking){
if($marking){ //for web route /customer/{marking}/payment-and-billing
$connection = CompanyConnection::where('invitee_reference', $marking)->first();
$company_module_id = $connection->invitee->id;
$filters = [
'per_page' => 999,
'status_in' => [2],
'receiver' => $company_module_id,
'type_in' => [1, 16]
'type_in' => [TransactionType::SHIPPING_INVOICE]
];
$transactions = $this->listsTransactions->execute($filters);
}
else{
else{ //for api route /transactions/list
$filters = json_decode($request->input('filters'), true);
$filters['per_page'] = 999;
unset($filters['does_not_have_payment_status_in']);
unset($filters['does_not_have_groups']);
$filters['type_in'] = [TransactionType::SHIPPING_INVOICE];
if(json_encode($filters['order_by']) == '{"column":"id","DESC":true}'){
Log::channel('storage_invoices')->info('CheckForStorageInvoiceByTransactions 3 Match');
$filters['order_by_id_desc'] = true;
}
unset($filters['order_by']);
$transactions = $this->listsTransactions->execute($filters);
}
if(isset($filters['check_for_storage_invoice'])){
foreach($transactions as $transaction){
$packingList = $transaction->owner()->first();
if($packingList){
$order = $packingList->owner()->first();
if($order instanceof Order){
$this->storageInvoiceTransactionProcessor->execute($order->reference);
$storages = $this->storageInvoiceTransactionProcessor->executeOrder($order);
if($storages){
$results = array_merge($results, $storages);
}
}
}
}
}
$filteredResults = array_values(array_filter($results, function($item, $key) {
static $seen = array();
$hash = md5($item['parentInvoiceId'] . $item['storageInvoiceId']);
return !isset($seen[$hash]) && ($seen[$hash] = true);
}, ARRAY_FILTER_USE_BOTH));
$request->merge(['storages' => $filteredResults]);
return $next($request);
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Resources;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
class GroupForOrderV2Resource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'original_amount' => (float) $this->original_amount,
'original_currency' => new CurrencyResource($this->original_currency),
'issuer_name' => $this->issuerCompany->name,
'issuer_id' => $this->issuerCompany->id,
'amount' => (float) $this->amount,
'service_charge' => (float) $this->amount,
'currency' => new CurrencyResource($this->currency),
'created_at' => Carbon::parse($this->created_at)->format('d-m-Y h:i:s A'),
'currency_rate' => (float) $this->currency_rate,
'status' => $this->status,
'status_name' => ApprovalStatus::APPROVAL_STATUS_ID[$this->status],
'payment_method' => (int)$this->payment_method,
'payment_method_name' => ucwords(PaymentMethodType::PAYMENT_METHODS_ID[$this->payment_method]),
'payment_reference' => $this->reference,
'transactions_ids' => GroupTransactionsForOrderV2Resource::collection($this->groupTransactions)
];
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class GroupTransactionsForOrderV2Resource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
// 'id' => $this->id,
'group_id' => $this->group_id,
'transaction_id' => $this->transaction_id,
];
}
}
+13 -3
View File
@@ -3,15 +3,14 @@
namespace App\Http\Resources;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\DocumentType;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\Group;
use App\Models\Order;
use App\Models\Transaction;
use App\Models\Wallet;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
class TransactionResource extends JsonResource
{
/**
@@ -24,6 +23,8 @@ class TransactionResource extends JsonResource
{
$order = null;
$groupTransactions = null;
$group_payment_attempts = null;
$group_payment_expired = null;
if ($this->owner instanceof Transaction) {
if ($this->owner) {
@@ -35,6 +36,12 @@ class TransactionResource extends JsonResource
if ($this->owner) {
$order = new OrderResource($this->owner->owner);
}
if($this->groups){
$group_payment_attempts = GroupForOrderV2Resource::collection($this->groups->whereNotIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]));
$group_payment_expired = GroupForOrderV2Resource::collection($this->groupsWithTrashed->whereIn('status', [ApprovalStatus::EXPIRED]));
}
} else {
$group = Group::where('reference', $this->payment_reference)->first();
if ($group) {
@@ -48,7 +55,9 @@ class TransactionResource extends JsonResource
'owner_type' => $this->owner_type,
'order' => $order,
'group_transactions' => $groupTransactions,
'group_reference' => $groupTransactions ? $group->reference : null,
'group_reference' => $groupTransactions ? ($group ? $group->reference : null ) : null,
'groups_payment_attempts' => $group_payment_attempts,
'groups_payment_expired' => $group_payment_expired,
'documents' => $groupTransactions ? DocumentResource::collection($this->documents->where('status', ApprovalStatus::PENDING_VERIFICATION)) : DocumentResource::collection($this->documents),
'type' => (int) $this->type,
'bill_no' => $this->bill_no,
@@ -83,6 +92,7 @@ class TransactionResource extends JsonResource
->get()
),
'remarks' => RemarkResource::collection($this->remarks),
'storages' => $this->storages ? $this->storages : null, //from middleware
'expires_on' => Carbon::parse($this->expires_on)->format('d-m-Y h:s:i'),
'updated_at' => Carbon::parse($this->updated_at)->format('d-m-Y'),
'created_at' => Carbon::parse($this->created_at)->format('d-m-Y')
+8
View File
@@ -89,6 +89,14 @@ class Transaction extends AbstractModel implements Documentable, Transactionable
return $this->BelongsToMany(Group::class, GroupTransaction::class, 'transaction_id');
}
/**
* @return BelongsToMany
*/
public function groupsWithTrashed(): BelongsToMany
{
return $this->BelongsToMany(Group::class, GroupTransaction::class, 'transaction_id')->withTrashed();;
}
public function convert_original_amount()
{
if($this->booking()->first()->fix_currency_id !== 1) {
+6
View File
@@ -54,6 +54,12 @@ return [
'days' => 14,
],
'storage_invoices' => [
'driver' => 'single',
'path' => storage_path('logs/laravel_storage_invoices.log'),
'level' => 'info',
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIsCreditTermToCompanyConnections extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('company_connections', function (Blueprint $table) {
$table->boolean('is_credit_term')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('company_connections', function (Blueprint $table) {
$table->dropColumn('is_credit_term');
});
}
}
@@ -6,7 +6,8 @@
<div v-show="!$store.getters.isLoading(section)">
<list-component :section="section" :endpoint="endpoint" :options="options">
<template slot="list" slot-scope="{data}">
<payments-billing-components v-if="endpoint == route('api.transaction.list')" :section="section" :data="data" :selectedInvoice="selectedInvoice" v-on:input="updateList($event)"></payments-billing-components>
<!-- <payments-billing-components v-if="endpoint == route('api.transaction.list') && !data.storages" :section="section" :data="data" :selectedInvoice="selectedInvoice" v-on:input="updateList($event)"></payments-billing-components> -->
<payments-billing-variant-2-components v-if="endpoint == route('api.transaction.list')" :section="section" :invoices="getMergedInvoices(data, data.storages)" :selectedInvoice="selectedInvoice" :isPaidInvoices="isPaidInvoices" v-on:input="updateList($event)"></payments-billing-variant-2-components>
<group-payments-billing-components v-else :section="section" :data="data" :selectedInvoice="selectedInvoice" v-on:input="updateList($event)"></group-payments-billing-components>
</template>
</list-component>
@@ -94,6 +95,10 @@
type: String,
required: true
},
isPaidInvoices :{
type: Boolean,
default: false
}
},
data(){
return {
@@ -117,7 +122,7 @@
},
selectedIds () {
return this.selectedInvoice.map(s=>s.id);
},
}
},
methods: {
generateSummaryInvoice(){
@@ -126,9 +131,40 @@
makePayment(){
this.submit(this.route('api.transaction.group.create', JSON.stringify(this.selectedId)), 'post', this.section, true, false);
},
updateList(packageList){
this.selectedInvoice.includes(packageList) ? this.selectedInvoice.splice(this.selectedInvoice.indexOf(packageList), 1) : this.selectedInvoice.push(packageList);
updateList(invoices){
invoices.forEach(invoice => {
if (this.selectedInvoice.includes(invoice)) {
this.selectedInvoice.splice(this.selectedInvoice.indexOf(invoice), 1);
} else {
this.selectedInvoice.push(invoice);
}
});
},
getMergedInvoices(shippingInvoice, storages){
if(storages){
const storageMap = {};
for (const storage of storages) {
storageMap[storage.parentInvoiceId] = storage.storageInvoiceId;
}
const storageInvoiceId = storageMap[shippingInvoice.id];
const storage = storages.find((i) => {
return i.storageInvoiceId === storageInvoiceId;
});
const mergedArray = [
...(shippingInvoice ? [shippingInvoice] : []),
...(storage && storage.storageInvoice.status !== 3 ? [storage.storageInvoice] : [])
];
return mergedArray;
}
const mergedArray = [
...(shippingInvoice ? [shippingInvoice] : [])
];
return mergedArray;
}
}
}
</script>
@@ -100,27 +100,27 @@
<div class="col bg-master-lightest p-1 p-sm-4">
<div class="row tabsContainer tabContent" tab-name="pendingPayment">
<div class="col">
<customer-payment-billing-inner-component :endpoint="route('api.transaction.list')" section="customerPendingPaymentInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [2], 'receiver': company_module_id, 'type_in': [1,16], 'does_not_have_payment_status_in': [0,1] ,'does_not_have_groups': 1}"></customer-payment-billing-inner-component>
<customer-payment-billing-inner-component :endpoint="route('api.transaction.list')" section="customerPendingPaymentInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [2], 'receiver': company_module_id, 'type_in': [1], 'does_not_have_payment_status_in': [0,1] ,'does_not_have_groups': 1, 'check_for_storage_invoice': 1}"></customer-payment-billing-inner-component>
</div>
</div>
<div class="row tabsContainer tabContent hide" tab-name="groupPaymentInProgress">
<div class="col">
<customer-payment-billing-inner-component :endpoint="route('api.transaction.group.list')" section="customerGroupPaymentInProgressInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [0, 1], 'receiver': company_module_id, order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
<customer-payment-billing-inner-component :endpoint="route('api.transaction.group.list')" section="customerGroupPaymentInProgressInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, 'status_in': [0, 1], 'receiver': company_module_id, order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
</div>
</div>
<div class="row tabsContainer tabContent hide" tab-name="groupPaymentExpired">
<div class="col">
<customer-payment-billing-inner-component :endpoint="route('api.transaction.group.list')" section="customerGroupPaymentExpiredInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [6], 'receiver': company_module_id, 'with_trashed': true, order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
<customer-payment-billing-inner-component :endpoint="route('api.transaction.group.list')" section="customerGroupPaymentExpiredInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, 'status_in': [6], 'receiver': company_module_id, 'with_trashed': true, order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
</div>
</div>
<div class="row tabsContainer tabContent hide" tab-name="paidGroupInvoice">
<div class="col">
<customer-payment-billing-inner-component :endpoint="route('api.transaction.group.list')" section="customerPaidGroupInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [2], 'receiver': company_module_id, order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
<customer-payment-billing-inner-component :endpoint="route('api.transaction.group.list')" section="customerPaidGroupInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, 'status_in': [2], 'receiver': company_module_id, order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
</div>
</div>
<div class="row tabsContainer tabContent hide" tab-name="paidInvoice">
<div class="col">
<customer-payment-billing-inner-component :endpoint="route('api.transaction.list')" section="customerPaidInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, order_by: {column: 'id', DESC: true}, 'status_in': [3], 'receiver': company_module_id, 'type_in': [1,16], order_by: {column: 'updated_at', DESC: true}}"></customer-payment-billing-inner-component>
<customer-payment-billing-inner-component :endpoint="route('api.transaction.list')" section="customerPaidInvoiceComponent" :company_module_id="company_module_id" :options="{'per_page': 10, 'status_in': [3], 'receiver': company_module_id, 'type_in': [1,16], order_by: {column: 'updated_at', DESC: true}}" :isPaidInvoices="true"></customer-payment-billing-inner-component>
</div>
</div>
</div>
@@ -147,9 +147,12 @@
<p>You can view your invoices here and make payment.</p>
</div>
</div>
<div class="row bg-master-lightest p-t-15" v-for="invoice in order.invoices">
<div class="col">
<customer-payments-billing-component :data="invoice" :storage="getStorage(invoice)" invoice_status="Pending Payment" :section="section"></customer-payments-billing-component>
<div class="row bg-master-lightest p-t-15" v-for="invoice in order.invoices" v-if="invoice.type === 1">
<div class="col" v-if="getMergedInvoices(invoice).length > 1">
<customer-payments-billing-variant-2-component :data="getMergedInvoices(invoice)" :storage="getStorageInfo(invoice)" invoice_status="Pending Payment" :section="section"></customer-payments-billing-variant-2-component>
</div>
<div class="col" v-else>
<customer-payments-billing-component :data="invoice" invoice_status="Pending Payment" :section="section"></customer-payments-billing-component>
</div>
</div>
</div>
@@ -180,6 +183,16 @@
computed: {
pendingQueue () {
return this.$store.getters.isInCompleteQueue(this.section);
},
orderStorageMap() {
// Create a map to link parent invoice IDs to storageInvoiceIds
const storageMap = {};
if(this.order.storages){
for (const storage of this.order.storages) {
storageMap[storage.parentInvoiceId] = storage.storageInvoiceId;
}
}
return storageMap;
}
},
watch: {
@@ -202,16 +215,35 @@
this.isLoading = false;
this.order = response.payload.data;
},
getStorage(invoice) {
const storageObject = this.findStorageObject(invoice.id);
getStorageInfo(invoice) {
const storageObject = this.findStorageInfoObject(invoice.id);
return storageObject;
},
findStorageObject(storageInvoiceId) {
findStorageInfoObject(shippingInvoiceId) {
if(this.order.storages){
return this.order.storages.find(storage => storage.storageInvoiceId === storageInvoiceId);
return this.order.storages.find(storage => storage.parentInvoiceId === shippingInvoiceId);
}
return null;
}
},
getMergedInvoices(invoice){
const storageInvoice = this.getStorageInvoice(invoice);
const mergedArray = [
...(invoice ? [invoice] : []),
...(storageInvoice ? [storageInvoice] : []),
];
return mergedArray;
},
getStorageInvoice(invoice) {
// Retrieve the storageInvoiceId for the current shipping invoice from storage info (order.storages)
const storageInvoiceId = this.orderStorageMap[invoice.id];
// Find and return the storage invoice (type 16) from order.invoices
const storageInvoice = this.order.invoices.find((invoice) => {
return invoice.type === 16 && invoice.id === storageInvoiceId;
});
return storageInvoice;
},
}
}
</script>
@@ -1,17 +1,6 @@
<template>
<div class="row m-b-15 m-l-5 m-r-10 parentContainer">
<div class="col bg-white rounded">
<div class="row">
<div class="col padding-20" v-if="storage" v-show="item.type === 16">
<div v-if="storage.numberOfDaysExceeded > 0">
If your items have been here for over 10 days after goods arrival in Malaysia, a storage fee of RM3 per cubic meter per day applies. Storage fees are recalculated daily, please make immediate payment before it expires at the end of the day.
</div>
<p >Warehouse Storage Fee (Days): {{ storage.numberOfDaysExceeded }} x 3 x {{ storage.cbm }}CBM</p>
</div>
<div class="col padding-20" v-show="item.type === 1 && item.status !== 3">
<p> Warehouse Storage Fee applies ten days after goods arrival in Malaysia.</p>
</div>
</div>
<div class="row" :class="[{'b-danger': item.status == 5 ||item.status == 6, 'b-a': item.status == 5||item.status == 6}]">
<div class="col padding-20">
<div class="row align-items-center">
@@ -213,9 +202,6 @@
section:{
type: String,
required: true
},
storage:{
type: Object
}
},
data(){
@@ -0,0 +1,297 @@
<template>
<div class="row m-b-15 m-l-5 m-r-10 parentContainer">
<div class="col bg-white rounded">
<div v-for="item in items" class="row" :class="[{'b-danger': item.status == 5 ||item.status == 6, 'b-a': item.status == 5||item.status == 6}]">
<div class="col padding-20">
<div class="row align-items-center">
<div class="col-2">
<p class="no-margin fs-10 all-caps">Invoice Date</p>
<div> {{ item.updated_at }}</div>
</div>
<div class="col">
<p class="no-margin fs-10 all-caps">Status</p>
<div class="all-caps" v-if="item.status == 3">Payment Completed</div>
<div class="all-caps text-danger" v-else-if="item.status == 5">Dispute in progress</div>
<div class="all-caps" v-else-if="item.status == 6">Cancelled Invoice</div>
<div class="all-caps" v-else>Pending Payment</div>
</div>
<div class="col-2">
<p class="no-margin fs-10 all-caps">Amount</p>
<div>MYR {{ item.amount.toFixed(2) }}</div>
</div>
<div class="col-3" v-if="item.remarks.length">
<p class="no-margin fs-10 all-caps">Billing Question</p>
<div>
{{ getLatestComment(item).content }}
<span class="btn requestModal no-border" v-if="item.remarks.length" size="large" data-type="chatmodal">
<i class="fa fa-comment-o"></i>
</span>
</div>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="chatmodal">
<remark-component :section="section" :data="item" module_type="Transaction"></remark-component>
</modal-component>
</div>
<div class="col-3" v-else>
<p class="no-margin fs-10 all-caps invisible">Billing Question</p>
<div>
<span class="btn requestModal no-border invisible">
<i class="fa fa-edit"></i>
</span>
</div>
</div>
<div class="col-auto p-l-0 p-r-0 d-flex justify-content-center align-items-center">
<div :class="[{'invisible': [5, 6, 3].includes(item.status)}]">
<span class="d-inline-block m-r-15 text-primary bold text-underline pointer requestModal" data-type="billingRemark">Billing Question?</span>
</div>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="billingRemark">
<customer-invoice-remark-form-component module_type="Transaction" :data="item" :section="section"></customer-invoice-remark-form-component>
</modal-component>
<div v-if="item.documents.length">
<div v-for="file in item.documents[0].files" v-bind:key="file.id" class="col-auto no-padding">
<document-file-viewer-component :file="file">
<template slot="button">
<div class="btn bg-grey no-border muted">
<i class="fa fa-file-pdf-o"></i>
</div>
</template>
</document-file-viewer-component>
</div>
</div>
<div v-else>
<div class="btn bg-grey no-border muted invisible">
<i class="fa fa-file-pdf-o"></i>
</div>
</div>
</div>
<div class="col-auto p-l-0 p-r-0 d-flex justify-content-center align-items-center" v-if="$store.getters.isSuperAdmin">
<span class="d-inline-block m-r-15 text-primary bold text-underline pointer requestModal" data-type="deleteInvoice">
<i class="fa fa-close"></i>
</span>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="deleteInvoice">
<delete-invoice-form-component :data="item" :section="section"></delete-invoice-form-component>
</modal-component>
</div>
<div class="col-auto hide">
<div class="btn btn-sm all-caps b-rad-none btn-block" :class="{'btn-success': !expanded, 'btn-default': expanded}" @click="expanded = !expanded">
{{ expanded ? 'Cancel' : 'Make Payment' }}</div>
</div>
</div>
<div class="row">
<div class="col" v-if="storage" v-show="item.type === 16">
<p >Warehouse Storage Fee: {{ storage.numberOfDaysExceeded }} Days x RM {{ storage.pricePerCBM}} x {{ storage.cbm }} cbm</p>
</div>
</div>
<div class="row b-t b-grey p-t-10 m-l-5 m-r-5" v-show="expanded" v-if="[5, 6].includes(item.status)">
<div class="col padding-20">
<div class="row bg-master-lightest h-100 padding-20">
<div class="col">
<h6 class="all-caps m-b-5 no-margin text-underline bold">Billing question</h6>
<remark-component :section="section" :data="item" module_type="Transaction"></remark-component>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row b-t b-grey p-t-10 m-l-5 m-r-5" >
<div class="col-12 col-md-7 padding-20">
<div class="row bg-master-lightest">
<div class="col">
<div v-for="(groupPaymentAttemptItem, index) in items" class="row bg-master-lightest" v-if="groupPaymentAttemptItem.groups_payment_attempts.length && groupPaymentAttemptItem.type == 1 && groupTransactionsExist(items, groupPaymentAttemptItem.groups_payment_attempts)">
<div class="col">
<div class="row m-t-10 m-b-10">
<div class="col">
<div v-if="index === 0" class="font-head fs-10 all-caps">Payment Attempt</div>
</div>
</div>
<div class="row">
<div class="col">
<shipping-transaction-component v-for="group in groupPaymentAttemptItem.groups_payment_attempts" v-bind:key="group.id" :data="group" :section="section"></shipping-transaction-component>
</div>
</div>
</div>
</div>
<div v-for="(groupPaymentAttemptItem, index) in items" class="row bg-master-lightest" v-if="groupPaymentAttemptItem.groups_payment_expired.length && groupPaymentAttemptItem.type == 1 && groupTransactionsExist(items, groupPaymentAttemptItem.groups_payment_expired)">
<div class="col">
<div class="row m-t-10 m-b-10">
<div class="col">
<div v-if="index === 0" class="font-head fs-10 all-caps">Payment Expired</div>
</div>
</div>
<div class="row">
<div class="col">
<payment-expired-component v-for="group in groupPaymentAttemptItem.groups_payment_expired" v-bind:key="group.id" :data="group" :section="section"></payment-expired-component>
</div>
</div>
</div>
</div>
<div v-for="(paymentAttemptItem, index) in items" class="row bg-master-lightest" v-if="items.some(x => x.payment_attempts && x.payment_attempts.length > 0)">
<div class="col">
<div class="row m-t-10 m-b-10">
<div class="col">
<div v-if="index === 0" class="font-head fs-10 all-caps">Payment Attempt</div>
</div>
</div>
<div class="row">
<div class="col">
<shipping-transaction-component v-for="i in paymentAttemptItem.payment_attempts" v-bind:key="i.id" :data="i" :section="section"></shipping-transaction-component>
</div>
</div>
</div>
</div>
<div v-for="(paymentExpiredItem, index) in items" class="row bg-master-lightest" v-if="items.some(x => x.payments_expired && x.payments_expired.length > 0)">
<div class="col">
<div class="row m-t-10 m-b-10">
<div class="col">
<div v-if="index === 0" class="font-head fs-10 all-caps">Payment Expired</div>
</div>
</div>
<div class="row">
<div class="col">
<payment-expired-component v-for="i in paymentExpiredItem.payments_expired" v-bind:key="i.id" :data="i" ></payment-expired-component>
</div>
</div>
</div>
</div>
<div v-for="(paymentHistoryItem, index) in items" class="row bg-master-lightest" v-if="items.some(x => x.payment_history && x.payment_history.length > 0)">
<div class="col">
<div class="row m-t-10 m-b-10">
<div class="col">
<div v-if="index === 0" class="font-head fs-10 all-caps">Payment History</div>
</div>
</div>
<div class="row">
<div class="col">
<payment-history-component v-for="i in paymentHistoryItem.payment_history" v-bind:key="i.id" :data="i" ></payment-history-component>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-5 padding-20 parentContainer">
<div class="row bg-master-lightest h-100">
<div class="col">
<div class="row padding-10">
<div class="col">
<div class="row align-items-end m-b-10 text-complete">
<div class="col">
<div class="font-heading all-caps fs-12">Total Amount:</div>
</div>
<div class="col-auto text-right">
<div class="font-heading fs-12">MYR {{(Math.round((totalAmount + Number.EPSILON) * 100) / 100).toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}}</div>
</div>
</div>
<div class="row align-items-end m-b-10 text-success">
<div class="col">
<div class="font-heading all-caps fs-12">Paid Total:</div>
</div>
<div class="col-auto text-right">
<div class="font-heading fs-12">MYR {{(Math.round((totalAmount - totalOutstanding + Number.EPSILON) * 100) / 100).toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}}</div>
</div>
</div>
<div class="row align-items-end m-b-10">
<div class="col">
<div class="font-heading all-caps fs-12">Floating Amount:</div>
</div>
<div class="col-auto text-right">
<div class="font-heading fs-12">MYR {{(Math.round((totalFloating + Number.EPSILON) * 100) / 100).toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}}</div>
</div>
</div>
<div class="row align-items-end bold text-danger">
<div class="col">
<div class="font-heading all-caps fs-12">OutStanding Total:</div>
</div>
<div class="col-auto text-right">
<div class="font-heading fs-12">MYR {{(Math.round((totalOutstanding + Number.EPSILON) * 100) / 100).toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}}</div>
</div>
</div>
<div class="row m-t-20" v-if="totalOutstanding - totalFloating > 0.009">
<div class="col">
<div class="btn btn-sm all-caps b-rad-none btn-success btn-block requestModal" data-type="makePayment">Make Payment</div>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="makePayment">
<group-payment-form-component :section="section" :selectedIds="selectedIds" :sumAmount="totalOutstanding.toFixed(2)"></group-payment-form-component>
</modal-component>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Array,
},
invoice_status: {
type: String,
required: true
},
section:{
type: String,
required: true
},
storage:{
type: Object
},
},
data(){
return {
items: this.data,
parameters: {
packing_list_id: null,
transaction_details: [],
},
expanded: true,
}
},
watch: {
data: function() {
this.items = this.data;
}
},
computed: {
totalAmount() {
return this.items.reduce((sum, item) => sum + item.amount, 0);
},
totalOutstanding() {
return this.items.reduce((sum, item) => sum + item.outstanding, 0);
},
totalFloating() {
return this.items.reduce((sum, item) => sum + item.floating, 0);
},
selectedIds() {
const ids = [];
this.items.forEach(item => {
ids.push(item.id);
});
return ids;
},
},
methods: {
getLatestComment(item) {
let questions = item.remarks;
return questions.slice().reverse()[0];
},
successHandler(response){
this.item = response.payload.data;
},
groupTransactionsExist(items, groups) {
return groups.some(group =>
group.transactions_ids.length === items.length &&
group.transactions_ids.every(transaction =>
items.some(item => item.id === transaction.transaction_id)
)
);
}
}
}
</script>
@@ -0,0 +1,110 @@
<template>
<div class="row m-b-15 m-l-5 m-r-10">
<div class="col bg-white rounded b-a" :class="{'b-white': !selected, 'b-primary': selected, 'bg-primary-lighter': selected}">
<div v-for="item in items" class="row" v-if="$store.getters.isAdmin || item.order">
<div class="col padding-20">
<div class="row align-items-center">
<div class="col-auto pointer align-items-center" style="min-width:70px;" @click="activate()" v-if="!['customerPaidGroupInvoiceComponent', 'customerGroupPaymentInProgressInvoiceComponent'].includes(section)">
<i v-show="((item.type === 1 && !isPaidInvoices) || isPaidInvoices)" class="fa fs-30 fa-fw" :class="{'fa-square-o': !selected, 'fa-check-square': selected, 'text-primary':selected}" ></i>
</div>
<div class="col">
<p class="no-margin fs-10 all-caps">Invoice No</p>
<div>{{ item.bill_no }}</div>
</div>
<div class="col">
<p class="no-margin fs-10 all-caps">Order</p>
<div v-if="item.order"><a :href="route('order.show', item.order.reference)">{{item.order.reference}}</a></div>
<div class="text-danger" v-else>Error in retrieving order</div>
</div>
<div class="col">
<p class="no-margin fs-10 all-caps">Invoice Date</p>
<div>{{ item.created_at }}</div>
</div>
<div class="col">
<p class="no-margin fs-10 all-caps">Amount</p>
<div>MYR {{ item.amount.toFixed(2) }}</div>
</div>
<div class="col" v-if="['customerPaidInvoiceComponent', 'customerPaidGroupInvoiceComponent'].includes(section)">
<p class="no-margin fs-10 all-caps">Payment Date</p>
<div>{{ item.payment_history[item.payment_history.length-1].created_at }}</div>
</div>
<div class="col" v-if="['customerPaidInvoiceComponent', 'customerPaidGroupInvoiceComponent'].includes(section)">
<p class="no-margin fs-10 all-caps">Bill Number</p>
<div>{{ item.payment_history[item.payment_history.length-1].payment_reference }}</div>
</div>
<div class="col-auto">
<div v-if="item.documents.length">
<div v-for="file in item.documents[0].files" v-bind:key="file.id" class="col-auto no-padding">
<document-file-viewer-component :file="file">
<template slot="button">
<div class="btn no-border muted" :class="{'btn-info': section === 'customerPaidInvoiceComponent', 'btn-success': section !== 'customerPaidInvoiceComponent'}">
<i class="fa fa-file-pdf-o"></i>
</div>
</template>
</document-file-viewer-component>
</div>
</div>
<div v-else>
<div class="btn bg-grey no-border muted invisible">
<i class="fa fa-file-pdf-o"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
selectedInvoice: {
type: Array,
required: false,
},
section:{
type: String,
default: null
},
invoices: {
type: Array,
},
isPaidInvoices :{
type: Boolean,
default: false
}
},
data(){
return {
items: this.invoices,
expanded: false,
selectedValue: false,
}
},
methods: {
activate(){
// this.select = !this.select;
this.$emit('input', this.invoices);
}
},
watch: {
invoices: function() {
this.items = this.invoices;
}
},
computed: {
selected() {
var response = false;
this.selectedInvoice.forEach((value, index) => {
this.items.forEach(item => {
if (item.id === value.id) {
response = true;
}
});
});
return response;
}
}
}
</script>
@@ -164,7 +164,7 @@
},
company_module_id: {
type: Number,
required: true,
// required: true,
},
selectedIds: {
type: Array,
@@ -25,12 +25,21 @@
@if($status === 2)
<div class="row">
<div class="col">
@if(!$result)
<div class="row m-b-10">
<div class="col">
<h5 class="semi-bold text-success">Your Payment is Unsuccessful</h5>
<p class="hint-text">Please contact our customer service.</p>
</div>
</div>
@else
<div class="row m-b-10">
<div class="col">
<h5 class="semi-bold text-success">Your Payment is Successful</h5>
<p class="hint-text">Thank you for your payment. The amount paid has been successfully applied to your invoice.</p>
</div>
</div>
@endif
<div class="row m-b-20">
<div class="col">
@if($marking)
+1
View File
@@ -42,6 +42,7 @@ Route::group(['prefix' => 'transactions', 'namespace' => 'Transactions', 'as' =>
Route::group(['prefix' => 'groups', 'as' => 'group.'], function () {
Route::post('/{transaction_ids}/create', 'CreateGroupsController@create')->name('create');
Route::get('/list', 'ListGroupsController@list')->name('list');
// Route::get('/list', 'ListGroupsController@list')->middleware('storage.invoice.check.bygroup')->name('list');
Route::delete('/{id}/delete', 'DeleteGroupController@delete')->name('delete');
Route::put('/{id}/update', 'UpdateGroupController@update')->name('update');
// Route::post('/{id}/approve', 'CreateBulkPurchaseOrderDocumentController@aprove')->name('approve');