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/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..0e8c3d88 --- /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)->where('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/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php b/app/Classes/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php index aa7f2ed7..969c2c15 100644 --- a/app/Classes/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php +++ b/app/Classes/Modules/Bookings/Processors/CreatePurchaseOrderFor1688OrderProcessor.php @@ -3,12 +3,15 @@ namespace App\Classes\Modules\Bookings\Processors; +use App\Classes\Exceptions\MalformedRequestException; +use App\Classes\Modules\Bookings\Services\Convert1688PurchaseOrderToProductList; 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\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\Booking; @@ -21,6 +24,9 @@ class CreatePurchaseOrderFor1688OrderProcessor /** @var CreatePurchaseOrderTransactionProcessor */ private $createPurchaseOrderTransactionProcessor; + /** @var Convert1688PurchaseOrderToProductList */ + private $convert1688PurchaseOrderToProductList; + /** @var UpdatesTransactionStatus */ private $updatesTransactionStatus; @@ -28,31 +34,38 @@ class CreatePurchaseOrderFor1688OrderProcessor private $createInvoiceTransactionProcessor; /** - * CreatePurchaseOrderFor1688OrderProcessor constructor. * @param GeneratesTransactionBillNumber $generatesTransactionBillNumber * @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor + * @param Convert1688PurchaseOrderToProductList $convert1688PurchaseOrderToProductList * @param UpdatesTransactionStatus $updatesTransactionStatus * @param CreateInvoiceTransactionProcessor $createInvoiceTransactionProcessor */ - public function __construct(GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, UpdatesTransactionStatus $updatesTransactionStatus, CreateInvoiceTransactionProcessor $createInvoiceTransactionProcessor) + public function __construct(GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, Convert1688PurchaseOrderToProductList $convert1688PurchaseOrderToProductList, UpdatesTransactionStatus $updatesTransactionStatus, CreateInvoiceTransactionProcessor $createInvoiceTransactionProcessor) { $this->generatesTransactionBillNumber = $generatesTransactionBillNumber; $this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor; + $this->convert1688PurchaseOrderToProductList = $convert1688PurchaseOrderToProductList; $this->updatesTransactionStatus = $updatesTransactionStatus; $this->createInvoiceTransactionProcessor = $createInvoiceTransactionProcessor; } + public function execute(Booking $booking){ $billNumber = $this->generatesTransactionBillNumber->execute('PO-'); - $products = [ - [ - 'description' => 'please refer to the attachment below', - 'quantity' => 1, - 'stockCode' => '', - 'unit_price' => $booking->fix_amount - ] - ]; + $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)) return false; $total = collect($products)->sum(function($product){ return $product['quantity'] * floatval(str_replace(',', '', $product['unit_price'])); @@ -65,9 +78,11 @@ class CreatePurchaseOrderFor1688OrderProcessor $purchaseOrder = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object); - $this->updatesTransactionStatus->execute($purchaseOrder, ApprovalStatus::APPROVED); + if($purchaseOrder->amount === $booking->fix_amount){ + $this->updatesTransactionStatus->execute($purchaseOrder, ApprovalStatus::APPROVED); + $this->createInvoiceTransactionProcessor->execute($booking); + } - $this->createInvoiceTransactionProcessor->execute($booking); } -} \ No newline at end of file +} 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/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/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/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/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/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue b/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue index 12d91e5d..d12c42b9 100644 --- a/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue +++ b/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue @@ -228,14 +228,14 @@
-

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.

-
- +
+