diff --git a/app/Classes/General/Abstracts/AbstractControllerLogic.php b/app/Classes/General/Abstracts/AbstractControllerLogic.php index b0e6e7bf..815219c8 100644 --- a/app/Classes/General/Abstracts/AbstractControllerLogic.php +++ b/app/Classes/General/Abstracts/AbstractControllerLogic.php @@ -16,6 +16,7 @@ use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; abstract class AbstractControllerLogic { @@ -66,6 +67,7 @@ abstract class AbstractControllerLogic return $response; } catch (ErrorException|GeneralExceptions $exception){ + log::error($exception); return (new ApiResponseObject($this->getNotificationTitle().' failed', $exception->getMessage(), $exception->getCode() ? $exception->getCode() : HttpStatus::SERVER_ERROR))->handler(); @@ -99,4 +101,4 @@ abstract class AbstractControllerLogic return $this->response(json_decode($collection->response()->getContent(), true)); } -} \ No newline at end of file +} diff --git a/app/Classes/General/Eloquent/AbstractListRecord.php b/app/Classes/General/Eloquent/AbstractListRecord.php index c5d2e6e6..7b7d0df9 100644 --- a/app/Classes/General/Eloquent/AbstractListRecord.php +++ b/app/Classes/General/Eloquent/AbstractListRecord.php @@ -6,6 +6,7 @@ namespace App\Classes\General\Eloquent; use App\Classes\Exceptions\MalformedRequestException; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\Log; abstract class AbstractListRecord extends AbstractGetRecord { @@ -23,6 +24,7 @@ abstract class AbstractListRecord extends AbstractGetRecord return $this->handler($filters); } catch (QueryException $exception){ + log::error($exception); throw new MalformedRequestException('Unable to fetch the list of records due to unexpected error'); } @@ -43,4 +45,4 @@ abstract class AbstractListRecord extends AbstractGetRecord } -} \ No newline at end of file +} diff --git a/app/Classes/General/Eloquent/Filters/CompanyIdNotIn.php b/app/Classes/General/Eloquent/Filters/CompanyIdNotIn.php new file mode 100644 index 00000000..bde6e0ea --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/CompanyIdNotIn.php @@ -0,0 +1,20 @@ +whereNotIn('company_id', $value); + } + +} diff --git a/app/Classes/General/Eloquent/Filters/DoesNotHavePurchaseOrderStatusIn.php b/app/Classes/General/Eloquent/Filters/DoesNotHavePurchaseOrderStatusIn.php new file mode 100644 index 00000000..8f8d305e --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/DoesNotHavePurchaseOrderStatusIn.php @@ -0,0 +1,24 @@ +whereDoesntHave('transactions', function($transaction) use($value) { + return $transaction->where('transactions.type', TransactionType::PURCHASE_ORDER)->whereIn('transactions.status', $value); + }); + } +} diff --git a/app/Classes/General/Eloquent/Filters/DoesNotHaveTransactionType.php b/app/Classes/General/Eloquent/Filters/DoesNotHaveTransactionType.php new file mode 100644 index 00000000..22ed9573 --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/DoesNotHaveTransactionType.php @@ -0,0 +1,22 @@ +whereDoesntHave('transactions', function($query) use($value) { + return $query->where('transactions.type', $value); + }); + } +} diff --git a/app/Classes/General/Eloquent/Filters/HasPaymentStatusIn.php b/app/Classes/General/Eloquent/Filters/HasPaymentStatusIn.php new file mode 100644 index 00000000..158dc0a5 --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/HasPaymentStatusIn.php @@ -0,0 +1,23 @@ +whereHas('transactions', function($query) use($value) { + return $query->where('transactions.type', TransactionType::PAYMENT)->whereIn('transactions.status', $value); + }); + } +} diff --git a/app/Classes/General/Eloquent/Filters/HasPurchaseOrderStatusIn.php b/app/Classes/General/Eloquent/Filters/HasPurchaseOrderStatusIn.php new file mode 100644 index 00000000..bdc32fa1 --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/HasPurchaseOrderStatusIn.php @@ -0,0 +1,23 @@ +whereHas('transactions', function($query) use($value) { + return $query->where('transactions.type', TransactionType::PURCHASE_ORDER)->whereIn('transactions.status', $value); + }); + } +} diff --git a/app/Classes/General/Eloquent/Filters/HasTransactionType.php b/app/Classes/General/Eloquent/Filters/HasTransactionType.php new file mode 100644 index 00000000..7b277dae --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/HasTransactionType.php @@ -0,0 +1,22 @@ +has('transactions', function($query) use($value) { + return $query->where('transactions.type', $value); + }); + } +} diff --git a/app/Classes/General/Eloquent/Filters/WithWallets.php b/app/Classes/General/Eloquent/Filters/WithWallets.php new file mode 100644 index 00000000..5b002922 --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/WithWallets.php @@ -0,0 +1,20 @@ +with('wallets'); + } +} diff --git a/app/Classes/General/Traits/LogData.php b/app/Classes/General/Traits/LogData.php new file mode 100644 index 00000000..de4b68fd --- /dev/null +++ b/app/Classes/General/Traits/LogData.php @@ -0,0 +1,47 @@ +table).'_logs'; + $relationshipColumn = Str::singular($model->table).'_id'; + + $originalData = $model->getRawOriginal(); + + $originalData[$relationshipColumn] = $originalData['id']; + unset($originalData['id']); + + if (!Schema::hasTable($tableName)) { + DB::statement('CREATE TABLE '.$tableName.' LIKE '.$model->table); + + $indexs = DB::select('SHOW INDEX FROM '.$tableName.';'); + + $removedIndexes = []; + foreach ($indexs as $index){ + if($index->Column_name === 'id' || in_array($index->Key_name, $removedIndexes)) continue; + DB::statement('ALTER TABLE '.$tableName.' drop index '.$index->Key_name); + $removedIndexes[] = $index->Key_name; + } + + DB::statement('ALTER TABLE '.$tableName.' ADD COLUMN `'.$relationshipColumn.'` BIGINT NOT NULL AFTER `id`'); + } + + DB::table($tableName)->insert($originalData); + + }); + } +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/AutoPurchaseOrderFillLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/AutoPurchaseOrderFillLogic.php index e32d9bb2..230e1bb8 100644 --- a/app/Classes/Modules/Bookings/ControllersLogic/AutoPurchaseOrderFillLogic.php +++ b/app/Classes/Modules/Bookings/ControllersLogic/AutoPurchaseOrderFillLogic.php @@ -64,8 +64,8 @@ class AutoPurchaseOrderFillLogic extends AbstractControllerLogic */ public function logic(Request $request) : JsonResponse { - $bookings = Booking::where(function($query){ - return $query->whereMonth('created_at', 05)->whereYear('created_at', 2022); + $bookings = Booking::where('service_id', '!=', 4)->where(function($query){ + return $query->whereMonth('created_at', 07)->whereYear('created_at', 2022); })->whereDoesntHave('transactions', function($q){ $q->where('type', TransactionType::PURCHASE_ORDER); $q->whereIn('status', [ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::APPROVED]); @@ -111,4 +111,4 @@ class AutoPurchaseOrderFillLogic extends AbstractControllerLogic return $this->response([]); } -} \ No newline at end of file +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php index d4ba9807..1140ecd9 100644 --- a/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php +++ b/app/Classes/Modules/Bookings/ControllersLogic/CreateBookingPaymentLogic.php @@ -129,7 +129,7 @@ class CreateBookingPaymentLogic extends AbstractControllerLogic /** @var Wallet $wallet */ $wallet = $booking->company->wallets()->first(); - if(round($wallet->amount) < round($amount, 2)){ + if((float) number_format(($wallet->amount - $amount),2) < 0){ throw new MalformedRequestException('Insufficient wallet balance. Please Top up your wallet.'); } @@ -161,4 +161,4 @@ class CreateBookingPaymentLogic extends AbstractControllerLogic } -} \ No newline at end of file +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/RegenerateInvoiceBookingLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/RegenerateInvoiceBookingLogic.php index 71ea7ba1..252412a2 100644 --- a/app/Classes/Modules/Bookings/ControllersLogic/RegenerateInvoiceBookingLogic.php +++ b/app/Classes/Modules/Bookings/ControllersLogic/RegenerateInvoiceBookingLogic.php @@ -10,6 +10,7 @@ use App\Classes\Modules\Transactions\Services\DeletesTransaction; use App\Classes\Modules\Documents\Services\DeletesDocument; use App\Classes\Modules\Transactions\Processors\CreateInvoiceTransactionProcessor; +use App\Classes\ValueObjects\Constants\DocumentType; use App\Http\Resources\BookingResource; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -57,7 +58,7 @@ class RegenerateInvoiceBookingLogic extends AbstractControllerLogic * @param CreateInvoiceTransactionProcessor $createInvoiceTransactionProcessor */ public function __construct( - CanFetchBooking $canFetchBooking, + CanFetchBooking $canFetchBooking, FetchesBooking $fetchesBooking, DeletesTransaction $deletesTransaction, UpdatesBookingStatus $updatesBookingStatus, @@ -86,7 +87,7 @@ class RegenerateInvoiceBookingLogic extends AbstractControllerLogic $this->canFetchBooking->passes(); $booking = $this->fetchesBooking->execute([ - 'id' => $request->route('id'), + 'id' => $request->route('id'), 'status' => ApprovalStatus::COMPLETED, 'with_transactions' => true] ); @@ -98,7 +99,7 @@ class RegenerateInvoiceBookingLogic extends AbstractControllerLogic $this->deletesTransaction->execute($row); } - $document = $booking->documents()->get(); + $document = $booking->documents()->whereIn('document_type', [DocumentType::PURCHASE_ORDER, DocumentType::INVOICE, DocumentType::DELIVER_ORDER, DocumentType::SUPPLIER_DELIVER_ORDER])->get(); foreach ($document as $key => $row) { $this->deletesDocument->execute($row); } @@ -108,4 +109,4 @@ class RegenerateInvoiceBookingLogic extends AbstractControllerLogic return $this->resourceResponse(new BookingResource($booking)); } -} \ No newline at end of file +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/UploadPurchaseOrderLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/UploadPurchaseOrderLogic.php index 1a99d852..4fc89d44 100644 --- a/app/Classes/Modules/Bookings/ControllersLogic/UploadPurchaseOrderLogic.php +++ b/app/Classes/Modules/Bookings/ControllersLogic/UploadPurchaseOrderLogic.php @@ -4,6 +4,7 @@ namespace App\Classes\Modules\Bookings\ControllersLogic; use App\Classes\General\Abstracts\AbstractControllerLogic; +use App\Classes\Modules\Bookings\Processors\CreatePurchaseOrderFor1688OrderProcessor; use App\Classes\Modules\Bookings\Services\FetchesBooking; use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject; use App\Classes\Modules\Documents\Services\ApprovesDocument; @@ -11,11 +12,15 @@ use App\Classes\Modules\Documents\Services\CreatesDocument; use App\Classes\Modules\Documents\Services\CreatesFiles; use App\Classes\Modules\Documents\Services\FetchesDocument; use App\Classes\Modules\Documents\Services\RejectsDocument; +use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject; use App\Classes\Modules\Transactions\Processors\CreateInvoiceTransactionProcessor; +use App\Classes\Modules\Transactions\Processors\CreatePurchaseOrderTransactionProcessor; use App\Classes\Modules\Transactions\Services\FetchesTransaction; +use App\Classes\Modules\Transactions\Services\GeneratesTransactionBillNumber; use App\Classes\Modules\Transactions\Services\UpdatesTransactionStatus; use App\Classes\ValueObjects\Constants\ApprovalStatus; use App\Classes\ValueObjects\Constants\DocumentType; +use App\Classes\ValueObjects\Constants\PaymentMethodType; use App\Classes\ValueObjects\Constants\TransactionType; use App\Models\Document; use Illuminate\Http\JsonResponse; @@ -43,18 +48,23 @@ class UploadPurchaseOrderLogic extends AbstractControllerLogic /** @var CreatesFiles */ private $createsFile; + /** @var CreatePurchaseOrderFor1688OrderProcessor */ + private $createPurchaseOrderFor1688OrderProcessor; + /** * UploadPurchaseOrderLogic constructor. * @param FetchesBooking $fetchesBooking * @param CreatesDocument $createsDocument * @param CreatesFiles $createsFile + * @param CreatePurchaseOrderFor1688OrderProcessor $createPurchaseOrderFor1688OrderProcessor */ - public function __construct(FetchesBooking $fetchesBooking, CreatesDocument $createsDocument, CreatesFiles $createsFile) + public function __construct(FetchesBooking $fetchesBooking, CreatesDocument $createsDocument, CreatesFiles $createsFile, CreatePurchaseOrderFor1688OrderProcessor $createPurchaseOrderFor1688OrderProcessor) { $this->fetchesBooking = $fetchesBooking; $this->createsDocument = $createsDocument; $this->createsFile = $createsFile; + $this->createPurchaseOrderFor1688OrderProcessor = $createPurchaseOrderFor1688OrderProcessor; } /** @@ -74,7 +84,11 @@ class UploadPurchaseOrderLogic extends AbstractControllerLogic $this->createsFile->execute($document, $object); + if(in_array($booking->company->id, [199, 510])){ + $this->createPurchaseOrderFor1688OrderProcessor->execute($booking); + } + return $this->response([]); } -} \ No newline at end of file +} diff --git a/app/Classes/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php b/app/Classes/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php new file mode 100644 index 00000000..fc7190cc --- /dev/null +++ b/app/Classes/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php @@ -0,0 +1,96 @@ +generatesTransactionBillNumber = $generatesTransactionBillNumber; + $this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor; + $this->convert1688PurchaseOrderToProductList = $convert1688PurchaseOrderToProductList; + $this->updatesTransactionStatus = $updatesTransactionStatus; + $this->createInvoiceTransactionProcessor = $createInvoiceTransactionProcessor; + } + + + public function execute(Booking $booking) + { + $billNumber = $this->generatesTransactionBillNumber->execute('PO-'); + + $documents = $booking->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->get(); + $products = []; + foreach ($documents as $document) { + foreach ($document->files as $file) { + try { + $products = $this->convert1688PurchaseOrderToProductList->execute($file); + } catch (MalformedRequestException $exception) { + continue; + } + } + } + + if (empty($products)){ + $object = new TransactionObject($billNumber, TransactionType::PURCHASE_ORDER, $booking->company->id, 1, + 1, PaymentMethodType::CASH, + 0, 0, $booking->fix_currency_id, $booking->fix_currency_id, + 1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, []); + $this->createPurchaseOrderTransactionProcessor->execute($booking, $object); + return false; + } + + $total = collect($products)->sum(function($product){ + return $product['quantity'] * floatval(str_replace(',', '', $product['unit_price'])); + }); + + $object = new TransactionObject($billNumber, TransactionType::PURCHASE_ORDER, $booking->company->id, 1, + 1, PaymentMethodType::CASH, + $total, $total, $booking->fix_currency_id, $booking->fix_currency_id, + 1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, $products); + + $purchaseOrder = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object); + + if($purchaseOrder->amount === $booking->fix_amount){ + $this->updatesTransactionStatus->execute($purchaseOrder, ApprovalStatus::APPROVED); + $this->createInvoiceTransactionProcessor->execute($booking); + } + + } + +} diff --git a/app/Classes/Modules/Bookings/Services/Convert1688PurchaseOrderToProductList.php b/app/Classes/Modules/Bookings/Services/Convert1688PurchaseOrderToProductList.php new file mode 100644 index 00000000..b956aa39 --- /dev/null +++ b/app/Classes/Modules/Bookings/Services/Convert1688PurchaseOrderToProductList.php @@ -0,0 +1,91 @@ +parseFile(Storage::disk('documents')->path($file->file->file_info->original->file)); + } catch (Exception $exception) { + throw new MalformedRequestException('Invalid file format.'); + } + + $text = $pdf->getText(); + if(str_contains($text, '订单详情单')) throw new MalformedRequestException('Can\'t process non english documents'); + + $tables = explode('Amount (yuan)', $text); + $footer = end($tables); + $footer = preg_split('(Shipping|运费)', $footer); + if(array_key_exists(1, $footer)){ + $footer = preg_split('/[\t\n]/', $footer[1]); + $shipping = (float) filter_var( $footer[0], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION ); + $discount = array_filter($footer, function($var) { return preg_match("/(Discount|优惠)/", $var); }); + $discount = (float) filter_var(reset($discount), FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION ); + } + + + array_shift($tables); + + foreach ($tables as $table){ + $products = preg_split('/(yuan\/|yuan \/|元\/|元 \/)/', str_replace("\r\n","",$table)); + + array_pop($products); + foreach ($products as $product){ + $product = preg_split('/[\t]/',$product); + + $adjuster = (count($product) - 7); + $descriptionIndex = 3 + $adjuster; + + $quantity = (int) str_replace("\n","",$product[$descriptionIndex + 2]); + $unitPrice = (float) str_replace("\n","",$product[$descriptionIndex + 3]); + + $productList[] = [ + 'stockCode' => '', + 'description' => str_replace("\n","",$product[$descriptionIndex]), + 'quantity' => $quantity, + 'unit_price' => $unitPrice, + 'total' => (float) str_replace("\n","",$product[$descriptionIndex + 3]) + ]; + } + } + + if($shipping > 0) { + $productList[] = [ + 'stockCode' => '', + 'description' => 'Shipping Fee', + 'quantity' => 1, + 'unit_price' => $shipping, + 'total' => $shipping + ]; + } + + if($discount > 0) { + $productList[] = [ + 'stockCode' => '', + 'description' => 'Discount', + 'quantity' => 1, + 'unit_price' => $discount * -1, + 'total' => $discount * -1 + ]; + } + + return $productList; + } +} diff --git a/app/Classes/Modules/Companies/ControllersLogic/FetchCompanyLogic.php b/app/Classes/Modules/Companies/ControllersLogic/FetchCompanyLogic.php index 5406d682..3c3fb088 100644 --- a/app/Classes/Modules/Companies/ControllersLogic/FetchCompanyLogic.php +++ b/app/Classes/Modules/Companies/ControllersLogic/FetchCompanyLogic.php @@ -52,10 +52,10 @@ class FetchCompanyLogic extends AbstractControllerLogic { $this->canFetchCompany->passes(); - $query = $this->fetchesCompany->execute(['id' => $request->route('id'), 'with_bookings' => true]); + $query = $this->fetchesCompany->execute(['id' => $request->route('id'), 'with_bookings' => true, 'with_wallets' => true]); return $this->resourceResponse(new CompanyResource($query)); } -} \ No newline at end of file +} diff --git a/app/Classes/Modules/Exports/Services/ExportsLeadsTransactions.php b/app/Classes/Modules/Exports/Services/ExportsLeadsTransactions.php new file mode 100644 index 00000000..4b8581d6 --- /dev/null +++ b/app/Classes/Modules/Exports/Services/ExportsLeadsTransactions.php @@ -0,0 +1,71 @@ +request = $request; + $this->listsCompanies = $listsCompanies; + } + + public function headings(): array + { + return [ + 'Agent Name', + 'User Name', + 'Contact Number', + 'Email', + 'Customer Code', + 'Register Date' + ]; + } + + /** + * @return \Illuminate\Support\Collection|mixed + */ + public function query() + { + $query = $this->listsCompanies->execute( + [ + 'business_type' => 2, + 'with_bookings' => true, + 'without_confirmed_payments' => true, + 'does_not_have_segments' => [5], + ] + ); + + return $query->toQuery(); + } + + /** + * @param $row + * @return array + */ + public function map($row): array + { + return [ + '', + $row->name, + strval($row->contacts->first()->phone), + // $row->contacts->first()->email, + $row->employees()->first() == null ? '' : $row->employees()->first()->email, + $row->reference, + $row->created_at->format('d-m-Y') + ]; + } +} diff --git a/app/Classes/Modules/Exports/Services/ExportsTransactions.php b/app/Classes/Modules/Exports/Services/ExportsTransactions.php index b7f0e5ad..6d6751bf 100644 --- a/app/Classes/Modules/Exports/Services/ExportsTransactions.php +++ b/app/Classes/Modules/Exports/Services/ExportsTransactions.php @@ -80,9 +80,9 @@ class ExportsTransactions implements FromQuery, WithHeadingRow, WithMapping, Sho $transactionTypes[$transaction->type], $transaction->issuerCompany->name, $transaction->reciver, - $transaction->currency_id === 1 ? 'MYR' : 'RMB', + $transaction->currency->short_code, $transaction->amount, - $transaction->original_currency_id === 1 ? 'MYR' : 'RMB', + $transaction->original_currency->short_code, $transaction->original_amount, $transaction->currency_rate, $transaction->tax, @@ -90,7 +90,7 @@ class ExportsTransactions implements FromQuery, WithHeadingRow, WithMapping, Sho $transactionStatus[$transaction->status], \PhpOffice\PhpSpreadsheet\Shared\Date::dateTimeToExcel($transaction->created_at), \PhpOffice\PhpSpreadsheet\Shared\Date::dateTimeToExcel($transaction->updated_at), - $marking +// $marking ]; } -} \ No newline at end of file +} diff --git a/app/Classes/Modules/Transactions/Processors/CreateInvoiceDocumentProcessor.php b/app/Classes/Modules/Transactions/Processors/CreateInvoiceDocumentProcessor.php index d6231e0c..ae57a626 100644 --- a/app/Classes/Modules/Transactions/Processors/CreateInvoiceDocumentProcessor.php +++ b/app/Classes/Modules/Transactions/Processors/CreateInvoiceDocumentProcessor.php @@ -6,7 +6,10 @@ use App\Classes\Modules\Documents\Services\CreatesDocument; use App\Classes\Modules\Documents\Services\CreatesFiles; use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject; use App\Classes\ValueObjects\Constants\ApprovalStatus; +use App\Classes\ValueObjects\Constants\DocumentType; +use App\Models\Document; use Meneses\LaravelMpdf\Facades\LaravelMpdf; +use Webklex\PDFMerger\Facades\PDFMergerFacade as PDFMerger; class CreateInvoiceDocumentProcessor { @@ -40,6 +43,24 @@ class CreateInvoiceDocumentProcessor $lowercaseDocumentType = strtolower($document_type); $order_pdf = LaravelMpdf::loadView('pages.pdfs.' . $lowercaseDocumentType, ['transaction' => $transaction, 'po_order_transaction' => $purchaseOrder, 'supplier' => $supplier]); + + if($purchaseOrder->booking->service_id === 4) { + $purchaseOrderDocuments = $purchaseOrder->booking->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->get(); + + + $oMerger = PDFMerger::init(); + $order_pdf->save(storage_path('app/documents/temp.pdf')); + + $oMerger->addPDF(storage_path('app/documents/temp.pdf'), 'all'); + foreach ($purchaseOrderDocuments as $document){ + $oMerger->addPDF(storage_path('app/documents/'.$document->files()->first()->file->file_info->original->file), 'all'); + } + + $oMerger->merge(); + + $order_pdf = $oMerger; + } + $document_object = new DocumentObject( $document_type, [chunk_split('data:application/pdf;base64,' . base64_encode($order_pdf->output()))], @@ -48,7 +69,9 @@ class CreateInvoiceDocumentProcessor $lowercaseDocumentType . 's' ); + /** @var Document $document */ $document = $this->createsDocument->execute($purchaseOrder->booking, $document_object); $this->createsFile->execute($document, $document_object); + } } diff --git a/app/Classes/Modules/Transactions/Processors/CreateProformaInvoiceTransactionProcessor.php b/app/Classes/Modules/Transactions/Processors/CreateProformaInvoiceTransactionProcessor.php index 3e7e8137..0a42bcfc 100644 --- a/app/Classes/Modules/Transactions/Processors/CreateProformaInvoiceTransactionProcessor.php +++ b/app/Classes/Modules/Transactions/Processors/CreateProformaInvoiceTransactionProcessor.php @@ -100,7 +100,7 @@ class CreateProformaInvoiceTransactionProcessor * @return void * @throws \App\Classes\Exceptions\MalformedRequestException */ - public function execute(Booking $booking) + public function execute(Booking $booking) { $po_order_transaction = $booking->transactions() @@ -129,14 +129,26 @@ class CreateProformaInvoiceTransactionProcessor $billNumber = $this->generatesTransactionBillNumber->execute('PROFORMA-'); - $payable_amount = $booking->transactions()->whereNotIn('status', [ApprovalStatus::REJECTED, ApprovalStatus::SUSPENDED])->payments()->sum('amount'); + $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()); + })->orWhere(function($query){ + return $query->whereIn('status', [ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]); + }); + })->sum('amount'); $booking_amount = $booking->fix_amount; $transaction = $booking->transactions() ->where('type', TransactionType::PAYMENT) ->first(); - $booking_currency_average_rate = $booking_amount / $booking->transactions()->whereNotIn('status', [ApprovalStatus::REJECTED, ApprovalStatus::SUSPENDED])->payments()->selectRaw('sum(amount - service_charge - tax) as sub_total')->get()->sum('sub_total'); + $booking_currency_average_rate = $booking_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()); + })->orWhere(function($query){ + return $query->whereIn('status', [ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]); + }); + })->selectRaw('sum(amount - service_charge - tax) as sub_total')->get()->sum('sub_total'); $total_service_charge = $booking->transactions() ->where('type', TransactionType::PAYMENT) diff --git a/app/Classes/Modules/Transactions/Processors/GeneratesGroupTransactionsPurchaseOrder.php b/app/Classes/Modules/Transactions/Processors/GeneratesGroupTransactionsPurchaseOrder.php index 70c84a89..80f218a6 100644 --- a/app/Classes/Modules/Transactions/Processors/GeneratesGroupTransactionsPurchaseOrder.php +++ b/app/Classes/Modules/Transactions/Processors/GeneratesGroupTransactionsPurchaseOrder.php @@ -86,4 +86,4 @@ class GeneratesGroupTransactionsPurchaseOrder } } -} \ No newline at end of file +} diff --git a/app/Classes/ValueObjects/Constants/RouteType.php b/app/Classes/ValueObjects/Constants/RouteType.php new file mode 100644 index 00000000..f2e43f78 --- /dev/null +++ b/app/Classes/ValueObjects/Constants/RouteType.php @@ -0,0 +1,11 @@ +download('leadsData.xls', Excel::XLS, ['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']); + ob_end_clean(); + return $response; + } } \ No newline at end of file diff --git a/app/Http/Controllers/Imports/ImportUpdateDebtorController.php b/app/Http/Controllers/Imports/ImportUpdateDebtorController.php index 312b55f2..1acc7b11 100644 --- a/app/Http/Controllers/Imports/ImportUpdateDebtorController.php +++ b/app/Http/Controllers/Imports/ImportUpdateDebtorController.php @@ -27,4 +27,4 @@ class ImportUpdateDebtorController Excel::import(new ImportsDebtor(), json_decode($object->getFiles()[0])->file_info->original->file); return []; } -} \ No newline at end of file +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 172d428b..7fa25aee 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -45,11 +45,13 @@ class Kernel extends HttpKernel \Illuminate\View\Middleware\ShareErrorsFromSession::class, VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\WebRouteLogs::class, ], 'api' => [ 'throttle:300,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\ApiRouteLogs::class, ], ]; diff --git a/app/Http/Middleware/ApiRouteLogs.php b/app/Http/Middleware/ApiRouteLogs.php new file mode 100644 index 00000000..984ff5ea --- /dev/null +++ b/app/Http/Middleware/ApiRouteLogs.php @@ -0,0 +1,64 @@ +fullUrl()); + $request_method = $request->method(); + $platform = $request->header('sec-ch-ua-platform'); + $browser = Str::after($request->header('sec-ch-ua'), 'Not?A_Brand";v="8", "Chromium";v="108", "'); + $geo = unserialize(file_get_contents("http://www.geoplugin.net/php.gp?ip=$ip_address")); + $longitude = $geo["geoplugin_longitude"]; + $latitude = $geo["geoplugin_latitude"]; + $last_page = url()->previous(); + $countryName = $geo["geoplugin_countryName"]; + + $user_id = Auth::user() ? Auth::user()->id : 0; + + // if ($user_id != 0) { + // // $ip_address + // $webRouteLogWithSameIp = RouteLog::where('ip_address', $ip_address)->where('user_id', 0)->get(); + // dd($webRouteLogWithSameIp); + // foreach($webRouteLogWithSameIp as $result){ + // $result + // } + // } + + $route_log = [ + 'user_id' => $user_id, + // 'ip_address' => $request->ip(), + 'ip_address' => $ip_address, + 'url' => $url, + 'request_method' => $request_method, + 'browser' => $browser, + 'platform' => $platform, + 'longitude' => $longitude, + 'latitude' => $latitude, + 'last_page' => $last_page, + 'countryname' => $countryName, + 'route_type' => RouteType::API, + ]; + + RouteLog::create($route_log); + + return $next($request); + } +} diff --git a/app/Http/Middleware/WebRouteLogs.php b/app/Http/Middleware/WebRouteLogs.php new file mode 100644 index 00000000..b5200de6 --- /dev/null +++ b/app/Http/Middleware/WebRouteLogs.php @@ -0,0 +1,55 @@ +fullUrl(); + $request_method = $request->method(); + $platform = $request->header('sec-ch-ua-platform'); + $browser = Str::after($request->header('sec-ch-ua'), 'Not?A_Brand";v="8", "Chromium";v="108", "'); + $geo = unserialize(file_get_contents("http://www.geoplugin.net/php.gp?ip=$ip_address")); + $longitude = $geo["geoplugin_longitude"]; + $latitude = $geo["geoplugin_latitude"]; + $last_page = url()->previous(); + $countryName = $geo["geoplugin_countryName"]; + + $route_log = [ + 'user_id' => Auth::user() ? Auth::user()->id : 0, + // 'ip_address' => $request->ip(), + 'ip_address' => $ip_address, + 'url' => $url, + 'request_method' => $request_method, + 'browser' => $browser, + 'platform' => $platform, + 'longitude' => $longitude, + 'latitude' => $latitude, + 'last_page' => $last_page, + 'countryname' => $countryName, + 'route_type' => RouteType::WEB, + ]; + + // dd($route_log); + + RouteLog::create($route_log); + + return $next($request); + } +} diff --git a/app/Http/Resources/BookingResource.php b/app/Http/Resources/BookingResource.php index 7d116db6..015a86ee 100644 --- a/app/Http/Resources/BookingResource.php +++ b/app/Http/Resources/BookingResource.php @@ -56,7 +56,7 @@ class BookingResource extends JsonResource ->whereDate('expires_on', '>=', Carbon::now()) ->get() ), - 'expired_payment_attempts' => TransactionResource::collection($this->transactions()->payments()->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '<', Carbon::now())->get()), + 'expired_payment_attempts' => TransactionResource::collection($this->transactions()->payments()->where('status', ApprovalStatus::PENDING_SUBMISSION)->whereDate('expires_on', '>=', Carbon::now())->where('expires_on', '>', Carbon::now()->toTimeString())->get()), 'payment_history' => TransactionResource::collection($this->transactions()->where(function($query){ $query->where(function($query){ $query->payments()->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::PENDING_VERIFICATION, ApprovalStatus::COMPLETED, ApprovalStatus::REJECTED]); diff --git a/app/Http/Resources/CompanyResource.php b/app/Http/Resources/CompanyResource.php index fceab5bc..cb357684 100644 --- a/app/Http/Resources/CompanyResource.php +++ b/app/Http/Resources/CompanyResource.php @@ -61,7 +61,7 @@ class CompanyResource extends JsonResource ], 'segments' => SegmentResource::collection($this->segments), 'services' => (new FetchesCompanyServices())->getServices($this->servicesConfigurations()), - 'wallet' => new WalletResource($this->wallets()->first()), + 'wallet' => $this->whenLoaded('wallets', new WalletResource($this->wallets()->with('transactions')->first()), new WalletResource($this->wallets()->first())), 'created_at' => $this->created_at->format('d-m-Y'), $this->mergeWhen($this->business_type === BusinessType::CURRENCY_VENDOR, [ 'currencies' => $segment ? CurrencyResource::collection(Currency::whereIn('id', $segment->detail->currencies)->get()) : [], diff --git a/app/Http/Resources/GroupResource.php b/app/Http/Resources/GroupResource.php index faae5b43..abd017ec 100644 --- a/app/Http/Resources/GroupResource.php +++ b/app/Http/Resources/GroupResource.php @@ -22,6 +22,9 @@ class GroupResource extends JsonResource public function toArray($request) { + if(!$this->issuerCompany){ + dd($this->id); + } return [ 'id' => $this->id, 'original_amount' => (float) $this->original_amount, diff --git a/app/Http/Resources/WalletResource.php b/app/Http/Resources/WalletResource.php index 143af1c4..4fa484ad 100644 --- a/app/Http/Resources/WalletResource.php +++ b/app/Http/Resources/WalletResource.php @@ -21,8 +21,8 @@ class WalletResource extends JsonResource 'currency_id' => $this->currency_id, 'amount' => (double) $this->amount, 'company_id' => (int) $this->owner->id, - 'transactions' => WalletTransactionResource::collection($this->transactions()->whereIn('status', [2, 3])->orderBy('id', 'DESC')->get()), - 'top_up_records' => WalletTransactionResource::collection($this->transactions()->whereNotIn('status', [0])->where('type', TransactionType::TOP_UP)->orderBy('id', 'DESC')->get()) + 'transactions' => $this->whenLoaded('transactions', WalletTransactionResource::collection($this->transactions()->whereIn('status', [2, 3])->orderBy('id', 'DESC')->get()), []), + 'top_up_records' => $this->whenLoaded('transactions', WalletTransactionResource::collection($this->transactions()->whereNotIn('status', [0])->where('type', TransactionType::TOP_UP)->orderBy('id', 'DESC')->get()), []), ]; } } diff --git a/app/Models/Booking.php b/app/Models/Booking.php index 2dfd945a..ca80d0c3 100644 --- a/app/Models/Booking.php +++ b/app/Models/Booking.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Classes\General\Interfaces\Documentable; use App\Classes\General\Interfaces\Transactionable; +use App\Classes\General\Traits\LogData; use App\Classes\ValueObjects\Constants\RoleTypes; use App\Scopes\CustomerBookingsScope; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -28,6 +29,7 @@ class Booking extends AbstractModel implements Documentable, Transactionable { use HasRelationships; use SoftDeletes; + use LogData; protected $table = 'bookings'; diff --git a/app/Models/Group.php b/app/Models/Group.php index 1fc6ca48..01dd257e 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Classes\General\Traits\LogData; use Illuminate\Database\Eloquent\Model; use App\Classes\General\Interfaces\Documentable; use Illuminate\Database\Eloquent\Relations\BelongsTo; diff --git a/app/Models/RouteLog.php b/app/Models/RouteLog.php new file mode 100644 index 00000000..5d99a604 --- /dev/null +++ b/app/Models/RouteLog.php @@ -0,0 +1,22 @@ + Illuminate\Support\Facades\View::class, 'PDF' => Barryvdh\DomPDF\Facade::class, 'MPDF' => Meneses\LaravelMpdf\Facades\LaravelMpdf::class, - 'GoogleReCaptchaV3'=> TimeHunter\LaravelGoogleReCaptchaV3\Facades\GoogleReCaptchaV3::class + 'GoogleReCaptchaV3'=> TimeHunter\LaravelGoogleReCaptchaV3\Facades\GoogleReCaptchaV3::class, + 'PDFMerger' => Webklex\PDFMerger\Facades\PDFMergerFacade::class ], diff --git a/database/migrations/2022_12_27_165842_create_route_logs_table.php b/database/migrations/2022_12_27_165842_create_route_logs_table.php new file mode 100644 index 00000000..bdab0257 --- /dev/null +++ b/database/migrations/2022_12_27_165842_create_route_logs_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('user_id')->unsigned(); + $table->string('ip_address'); + $table->string('url', 255); + $table->string('request_method'); + $table->string('browser')->nullable(); + $table->string('platform')->nullable(); + $table->string('longitude')->nullable(); + $table->string('latitude')->nullable(); + $table->string('last_page')->nullable(); + $table->string('countryname')->nullable(); + $table->integer('route_type'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('route_logs'); + } +} diff --git a/database/seeds/DummyDataSeeder.php b/database/seeds/DummyDataSeeder.php new file mode 100644 index 00000000..47196db2 --- /dev/null +++ b/database/seeds/DummyDataSeeder.php @@ -0,0 +1,508 @@ +faker = $faker; + $this->createsUser = $createsUser; + $this->createsCompany = $createsCompany; + $this->createsContact = $createsContact; + $this->createsAddress = $createsAddress; + $this->assignEmployeeProcessor = $assignEmployeeProcessor; + $this->createsDocument = $createsDocument; + $this->createsFiles = $createsFiles; + } + + + /** + * Run the database seeds. + * + * @return void + * @throws MalformedRequestException + * @throws AccessForbiddenException + * @throws RequestValidationException + */ + public function run() + { + // Local Development Default Password Hash + $password = '123456abcabc'; + + // At the moment we only have 3 different user roles: + // RoleTypes::SUPER_ADMIN : Full access, at the moment is not attached to a company but should be in the future. + // RoleTypes::ADMIN : Full access except for some sensitive features that require higher level of approval, at the moment is not attached to a company but should be in the future. + // RoleTypes::USER : This is the customer, can only access their own orders only, must be attached to a company. + + // User Status + // ApprovalStatus::PENDING_VERIFICATION : This should be the default status before the user verifies their email status, but currently this is not being implemented. + // ApprovalStatus::APPROVED : This is the status of users with verified emails. + // ApprovalStatus::SUSPENDED : This is the status if the users is blocked from the system, but currently this is not being implemented. + + + // =============================================== // + // Create CIEF Entities // + // =============================================== // + + // create super admin + $userObject = new RegistrationObject($this->faker->name, 'super_admin@izyim.com', $password, $password,RoleTypes::SUPER_ADMIN, ApprovalStatus::APPROVED); + $this->createsUser->execute($userObject); + + // create admin + $userObject = new RegistrationObject($this->faker->name, 'admin@izyim.com', $password, $password,RoleTypes::ADMIN, ApprovalStatus::APPROVED); + $this->createsUser->execute($userObject); + + // create CIEF + $company_object = new CompanyObject('CIEF Worldwide Sdn Bhd', 'CIEF',CompanyType::COMPANY_BUSINESS,ApprovalStatus::APPROVED); + /** @var Company $company */ + $company = $this->createsCompany->execute($company_object); + + // =============================================== // + // Create Supplier Entities // + // =============================================== // + // supplier entities consist of 2 type of company module [BusinessType::FREIGHT_FORWARDER, BusinessType::FREIGHT_FORWARDER, BusinessType::WAREHOUSE] + // in this use case we are creating 3 supplier, with each supplier having 6 company modules, 1 BusinessType::FREIGHT_FORWARDER and 5 BusinessType::WAREHOUSE. 1 warehouse for each location. + + for ($i = 1; $i <= 3; $i++) { + $supplierName = $this->faker->company; + $supplierReference = $this->faker->bothify('??-????'); + + $company_object = new CompanyObject($supplierName, $supplierReference,CompanyType::COMPANY_BUSINESS,ApprovalStatus::APPROVED); + /** @var Company $company */ + $company = $this->createsCompany->execute($company_object); + + $companyModuleObject = new CompanyModuleObject($supplierName, $supplierReference, '', '', BusinessType::FREIGHT_FORWARDER, ApprovalStatus::APPROVED); + $this->createsCompanyModule->execute($company, $companyModuleObject); + + // create supplier warehouses + foreach(['Guangzhou', 'Yiwu', 'Klang', 'Sabah', 'Sarawak'] as $name){ + + $warehouseReference = ''; + $isChina = false; + + switch($name) { + case 'Guangzhou': $warehouseReference = 'GZ-V0'.$i; $isChina = true; break; + case 'Yiwu': $warehouseReference = 'YY-V0'.$i; $isChina = true; break; + case 'Klang': $warehouseReference = 'KL-V0'.$i; break; + case 'Sabah': $warehouseReference = 'SB-V0'.$i; break; + case 'Sarawak':$warehouseReference = 'SRW-V0'.$i; break; + } + + $companyModuleObject = new CompanyModuleObject($name, $warehouseReference, '', '', BusinessType::WAREHOUSE, ApprovalStatus::APPROVED); + /** @var CompanyModule $companyModule */ + $companyModule = $this->createsCompanyModule->execute($company, $companyModuleObject); + + $address = new AddressObject( $this->faker->streetAddress, $this->faker->streetAddress, $isChina ? 2 : 1, $isChina ? 35 : 15, $isChina ? 633 : 412, $isChina ? 510450 : 41400, AddressType::DELIVERY, ApprovalStatus::APPROVED); + $this->createsAddress->execute($companyModule, $address); + + $contact = new ContactObject($this->faker->name, $this->faker->phoneNumber, '', ''); + $this->createsContact->execute($companyModule, $contact); + } + } + + + // =============================================== // + // Create Customer // + // =============================================== // + // 1. create user + // 2. create company + + // 3. Attach Employee + // 4. create contact + // 5. create Address + + // 6. identification verification + + // =============================================== // + // Wallet // + // =============================================== // + + // 7. top up wallet + + // =============================================== // + // Order Workflow // + // =============================================== // + + // 8. create recipient bank + // 9. create booking + // 10. make payment (Manual, FPX, Wallet) + // 11. approve payment (For manual payments only) * N + // 12. create supplier order + // 13. upload china payment proof (outsource * N) + // 14. create purchase order (maybe outsource) + // 15. approve purchase order () + // 16. generate invoice + + // generate random number of users + for($userLoop=1; $userLoop <= rand(20, 50); $userLoop++) { + + // === // + // 1 // ========== // + // Create user // + // ================= // + $customerName = $this->faker->name; + $customerEmail = $this->faker->email; + $userObject = new RegistrationObject($customerName, $customerEmail, $password, $password, RoleTypes::USER, ApprovalStatus::APPROVED); + /** @var User $user */ + $user = $this->createsUser->execute($userObject); + + // === // + // 2 // ========== // + // Create Company // + // ================= // + + // company reference is called marking, it is the human readable id. + + // CompanyTypes + // CompanyType::COMPANY_BUSINESS : For SME Business Entities and requires SSM for identity verification. + // CompanyType::PERSONAL_BUSINESS : For Personal Entities and requires IC for identity verification, and the company name will follow the customer name in this case. + + // Company Status + // ApprovalStatus::APPROVED : This is the default status of registered company. + // ApprovalStatus::SUSPENDED : This is the status if the company is blocked from releasing packages from warehouse due to pending verification. + + $isCompany = $this->faker->numberBetween(0, 1); + $companyName = $isCompany ? $this->faker->company : $customerName; + + $company_object = new CompanyObject($companyName, + mt_rand(1000, 9999).(new GeneratesInitials())->name($companyName)->length(3)->generate(), + $isCompany ? CompanyType::COMPANY_BUSINESS : CompanyType::PERSONAL_BUSINESS, + ApprovalStatus::APPROVED); + + /** @var Company $company */ + $company = $this->createsCompany->execute($company_object); + + // === // + // 3 // ===========// + // Attach Employee // + // ==================// + // employees are attached to company modules not companies, because an employee maybe working for one or many "Departments". + $Object = new EmploymentObject($company, $user); + $this->assignEmployeeProcessor->execute($Object); + + // === // + // 4 // ========== // + // Create Contact // + // ================= // + // Contacts uses eloquent polymorphic relationship to declare its owner. and for this use case it will be attached to the company not the company module. + $contactObject = new ContactObject($company->id, $customerName, $this->faker->phoneNumber, $customerEmail, null, $this->faker->bothify('??#####')); + $this->createsContact->execute($contactObject); + + // === // + // 5 // ========== // + // Create Address // + // ================= // + // Addresses uses eloquent polymorphic relationship to declare its owner. and for this use case it will be attached to the company module. + // an address has at least 1 contact for the PIC. + // there is 1 type of address we use: + // AddressType::BILLING : for the invoice billing address + + // create delivery address + $addressObject = new AddressObject($this->faker->streetAddress, '', 1, $this->faker->numberBetween(1, 15), $this->faker->numberBetween(1, 442), $this->faker->postcode); + /** @var Address $address */ + $address = $this->createsAddress->execute($company, $addressObject); + + // === // + // 6 // ====================== // + // identification verification // + // ============================== // + // please refer to company types section for more insight + $object = new DocumentObject($isCompany ? DocumentType::SSM_REGISTRATION : DocumentType::IDENTITY_CARD, ['data:image/png;base64,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'], + $isCompany ? $this->faker->bothify('SSM-#######') : $this->faker->bothify('############'), ApprovalStatus::APPROVED, 'identifications'); + /** @var Document $document */ + $document = $this->createsDocument->execute($company, $object); + $this->createsFiles->execute($document, $object); + + // === // + // 7 // ========= // + // top up wallet // + // ================ // + // when a customer tries to top up their wallet, if the wallet doesn't already exist it will be automatically created. + // wallet credit can be used to pay for transfer orders to enjoy better conversion rates. + // wallet top-ups can only be performed using FPX at the moment. but super admin can manually credit or debit credit to a customer's wallet + + // The transaction table is considered the most confusing part of our database because it is being used by multiple model using a polymorphic relationship + // and is used for many use cases in our application which is an unintended flaw, and we are looking for ways to improve it. + + + // A wallet top up is TransactionType::TOP_UP, there are many types of transactions used by a wallet: + // TransactionType::TOP_UP : represent a top-up amount to a wallet; + // TransactionType::PAYMENT : represent payment out of the wallet; + // TransactionType::CREDIT_NOTE : represent a manual top-up to a wallet, and can only be performed by super admin; + // TransactionType::CREDIT_NOTE : represent a deduction from a wallet, and can only be performed by super admin; + // TransactionType::WITHDRAW : represent a customer withdrawing credit out of a wallet to a bank account (refund); + + + // top up only some customers + $shouldTopUp = $this->faker->numberBetween(0, 1); + if($shouldTopUp) { + $object = new WalletObject($company->id, 1, $this->generatesWalletCode->execute()); + /** @var Wallet $wallet */ + $wallet = $this->createsWallet->execute($object, $company); + + $amount = $this->faker->numberBetween(10, 300000); + $billNumber = $this->generatesTransactionBillNumber->execute('TOPUP-'); + + // create billplz bill using api, we will skip this part in the seed. + $billPlzBill = $this->faker->bothify('???#####'); + + $transaction_object = new TransactionObject($billNumber, TransactionType::TOP_UP, 1, $company->id, 1, PaymentMethodType::PAYMENT_GATEWAY, $amount, $amount, 1, 1, 1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, [], $billPlzBill); + + /** @var Transaction $transaction */ + $transaction = $this->createsTransaction->execute($wallet, $transaction_object); + + + // on billplz callback url + $status = $this->faker->randomElement([ApprovalStatus::APPROVED, ApprovalStatus::REJECTED]); + $this->updatesTransactionStatus->execute($transaction, $status); + if($status === ApprovalStatus::APPROVED) { + $this->updatesWalletBalance->execute($wallet, $amount); + } + } + + // === // + // 8 // ========================== // + // Create Recipient Bank Accounts // + // ================================= // + // bank accounts are used to store bank account details, and can be used in a variety of ways + // Bank types: + // 1. PERSONAL : belong to the same entity + // 2. EXTERNAL : Doesn't belong to the entity, belongs to an external entity; + // 3. ALIPAY : : Is an external entity, but flag the type of bank as alipay e-wallet; + // + // here are some of the current use cases for banks in our application: + // 1. Recipient bank (EXTERNAL) (the account the customer is requesting to transfer funds to) + // 2. AliPay Transfer (EXTERNAL) (the account the customer is requesting to transfer funds to when bank type is ALIPAY) + // 3. Refund bank (PERSONAL) (the account the customer is requesting his order refunds to be transferred to) + $bank_object = new BankObject($company->id, $this->faker->numberBetween(2, 3), + $this->faker->bank, $this->faker->name, $this->faker->bankAccountNumber, + $this->faker->city, null, null, + 2, $this->faker->company); + + // todo create multiple bank accounts with multiple types + $bank = $this->createsBank->execute($bank_object); + + // generate random number of bookings + for($orderLoop=1; $orderLoop <= rand(1, 30); $orderLoop++) { + + // === // + // 9 // ========= // + // Create Booking // + // ================ // + // A booking is simply a transfer order to a supplier/manufacturer bank account overseas + // to pay for goods they are buying from overseas. the booking is not proceed until the + // customer requests to make a payment, when the customer start the payment process he + // will receive a quote for the cost to transfer the booked amount (e.g. 100 USD) in RM + + // bookings require 2 actions from the customer to be completed + // 1. Make Full payment ** + // 2. Provide Purchase Order (itemized list of the products they are buying) + + // ** A booking will be the sum of payments transferred to one bank account + // but can be partially paid (e.g. 1000 USD can be paid: $300 deposit + $700 balance) + // A shipping label can be re-used, and each batch that arrives at the supplier warehouse + // is referred to as a packing list. more on this later. + + // service types are configured by the super admin from the settings + // it will include things like conversion rates, service charge, etc.. + // and can be used to place different type of transfer orders (e.g. 1 day transfer, 3 days transfer, 1688 Payment) + + // randomly selects a service type + $service = ServiceType::where('reference', $this->faker->numberBetween(1, 3))->first(); + + // Booking human readable id + $reference = $this->generatesBookingMarking->execute(); + + // random currency booking (CNY, USD) + $bookedCurrency = $this->faker->numberBetween(2, 3); + $object = new BookingObject($service->id, $bank->id, $reference, $this->faker->numberBetween(10, 300000), $bookedCurrency, $bookedCurrency, 1); + + $booking = $this->createsBooking->execute($company, $object); + + + // === // + // 10 // ====== // + // make payment // + // ============== // + // There are few type of transactions related to a booking: + // TransactionType::PAYMENT : is used for 2 type of use cases (1. payments to transfer orders, 2. payment out of wallet) and is attached to a booking; + // TransactionType::BILL : is to represent the payment out to CIEF currency supplier (expenses) and is attached to a transaction of type TransactionType::PAYMENT; + // TransactionType::TRANSFER_FEE : is to represent the transfer fee charged by CIEF currency supplier is attached to a transaction type TransactionType::BILL; + // TransactionType::REFUND : is to represent a request for refund on a payment, and is attached to a transaction type TransactionType::PAYMENT; + + // todo make payment + // todo approve payment + + // todo create supplier order + // when processing a customer order, we will place an order with one of our currency supplier which will generate a transaction type TransactionType::BILL + // and attach it to the customer payment TransactionType::PAYMENT, and it will update the TransactionType::PAYMENT status to ApprovalStatus::COMPLETED + + // todo upload china payment proof + // when our currency supplier completes the transfer they will send us the bank slip as proof of payment, then the admin user + // will upload the bank slip document and attaching it to transaction type TransactionType::BILL + + // todo create purchase order + // creating the purchase order can happen before or after the payment is made, the customer needs to fill up the list of product + // they are buying and attaching it to the booking, a purchase order is a transaction of type TransactionType::PURCHASE_ORDER + // todo approve purchase order + + // todo generate invoice + // the invoicing documents will be generated once they 2 conditions are met: + // 1. Full payment completed (completed is flagged when the china payment proof is uploaded) + // 2. The purchase order is filled and approved (when the purchase order is not filled for more than 2 months the system will automatically generate a random products for Purchase order to close the order) + + // once the invoice is generated the transaction table will include 2 new transaction type TransactionType::INVOICE, TransactionType::SUPPLIER_DELIVERY + // and for documents will be generated and attached to the booking. + // once this process is complete the booking status will update to ApprovalStatus::COMPLETED + + } + + } + + } + +} diff --git a/resources/assets/vue/components/banks/forms/BankAccountFormComponent.vue b/resources/assets/vue/components/banks/forms/BankAccountFormComponent.vue index 3bf7fa65..6e9518d9 100644 --- a/resources/assets/vue/components/banks/forms/BankAccountFormComponent.vue +++ b/resources/assets/vue/components/banks/forms/BankAccountFormComponent.vue @@ -14,56 +14,108 @@ {{error}} -
Due to the system upgrades, you are no longer required to fill out the Purchase Order. We apologize that the invoices won't be available until the system has been fully upgraded. Thanks for your patience.
+You are no longer required to key in your purchase order details manually, the system will automatically get your purchase order detail from 1688 directly.
| - | '; + echo ''.$group->created_at.' | '; echo 'warning, booking ref.'.$booking->marking.' doesn\'t have purchase order | '; echo '0 | '; echo '0 | '; @@ -227,6 +236,7 @@ Route::get('/supplier/pi/export', function(){ $i++; echo '
| '.$i.' | '; + echo ''.$group->created_at.' | '; echo ''.$item->product_name.' | '; echo ''.$item->quantity.' | '; echo ''.$item->price * (1/$group->currency_rate).' | '; @@ -260,8 +270,8 @@ Route::get('/pending_orders', function(){ $i = 0; foreach ($payments as $payment){ $booking = $payment->owner; - if($booking->service_id === 4){ - continue; + if(!$booking instanceof Booking){ + dd($payment); } $bankType = str::length($booking->bank->holder_name) > 4 ? 'Company' : 'Personal'; @@ -343,11 +353,11 @@ Route::get('/wallet/audit', function (Request $request) { if((int) $transaction->type === TransactionType::PAYMENT) $payments += (float) $transaction->amount; if((int) $transaction->type === TransactionType::DEBIT_NOTE) $debit += (float) $transaction->amount; } - if(round((float) $wallet->amount - (($topups + $credit) - ($payments + $debit)), 2) == 0) continue; + if((round((float) $wallet->amount - (($topups + $credit) - ($payments + $debit)), 2) == 0) AND $wallet->amount > -0.01) continue; $i++; - echo $i.". Marking: ". $wallet->owner->reference ."