diff --git a/app/Classes/General/Eloquent/Filters/DateRange.php b/app/Classes/General/Eloquent/Filters/DateRange.php new file mode 100644 index 00000000..798b2d4d --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/DateRange.php @@ -0,0 +1,110 @@ +startOfDay() + : Carbon::now()->subMonth()->startOfDay(); + + $endDate = isset($dt2) && $dt2 + ? Carbon::createFromFormat('Y-m-d', $dt2)->endOfDay() + : Carbon::now()->endOfDay(); + + $minAllowedDate = Carbon::createFromFormat('Y-m-d', '2025-07-01')->startOfDay(); + if ($startDate->lt($minAllowedDate) || $endDate->lt($minAllowedDate)) { + $startDate = Carbon::createFromFormat('Y-m-d', '1970-01-01')->startOfDay(); + $endDate = Carbon::createFromFormat('Y-m-d', '1970-01-01')->startOfDay(); + } + + // return $builder->with([ + // 'company', + // 'transactions.transactionDetails', + // 'transactions.voucherRedemption', + // ]) + // ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + // $query->payments() + // ->complete() + // ->whereBetween('created_at', [$startDate, $endDate]) + // ->latest('created_at'); + // }) + // ->whereDoesntHave('transactions', function ($query) use ($startDate, $endDate) { + // $query->payments() + // ->complete() + // ->where(function ($q) use ($startDate, $endDate) { + // $q->where('created_at', '<', $startDate) + // ->orWhere('created_at', '>', $endDate); + // }); + // }); + + return $builder->whereIn('status', [ + ApprovalStatus::APPROVED, + ApprovalStatus::COMPLETED, + ApprovalStatus::SUSPENDED, + ]) + ->where(function ($q) use ($startDate, $endDate) { + $q->where(function ($q) { + $q->whereDoesntHave('company', function ($query) { + $query->where('debtor', 'N/A NO NEED TO IMPORT'); + }); + }) + // Bookings without refunds + ->where(function ($q1) use ($startDate, $endDate) { + $q1->where('status', ApprovalStatus::COMPLETED) + ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + $query->payments() + ->complete() + ->whereBetween('created_at', [$startDate, $endDate]); + }) + ->whereDoesntHave('transactions', function ($query) { + $query->payments() + ->complete() + ->where(function ($q) { + $q->where('created_at', '<', '2025-07-01 00:00:00'); + }); + }); + }) + // OR bookings with refunds + ->orWhere(function ($q2) use ($startDate, $endDate) { + $q2->whereHas('transactions', function ($query) use ($startDate, $endDate) { + $query->payments() + ->whereBetween('created_at', [$startDate, $endDate]) + ->whereHas('transactions', function ($refundQuery) { + $refundQuery->refunds() + ->whereIn('status', [ApprovalStatus::APPROVED]); + }); + }) + ->whereDoesntHave('transactions', function ($query) { + $query->payments() + ->complete() + ->where(function ($q) { + $q->where('created_at', '<', '2025-07-01 00:00:00'); + }); + }); + }); + }) + ->with([ + 'company', + 'transactions.transactionDetails', + 'transactions.voucherRedemption', + ]); + } +} diff --git a/app/Classes/General/Eloquent/Filters/DateRangeNoData.php b/app/Classes/General/Eloquent/Filters/DateRangeNoData.php new file mode 100644 index 00000000..9c2fc6eb --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/DateRangeNoData.php @@ -0,0 +1,145 @@ +startOfDay() + : Carbon::now()->subMonth()->startOfDay(); + + $endDate = isset($dt2) && $dt2 + ? Carbon::createFromFormat('Y-m-d', $dt2)->endOfDay() + : Carbon::now()->endOfDay(); + + $minAllowedDate = Carbon::createFromFormat('Y-m-d', '2025-07-01')->startOfDay(); + if ($startDate->lt($minAllowedDate) || $endDate->lt($minAllowedDate)) { + $startDate = Carbon::createFromFormat('Y-m-d', '1970-01-01')->startOfDay(); + $endDate = Carbon::createFromFormat('Y-m-d', '1970-01-01')->startOfDay(); + } + + // return $builder->with([ + // 'company', + // 'transactions.transactionDetails', + // 'transactions.voucherRedemption', + // ]) + + // // ->whereHas('company', function ($query) { + // // $query->where('debtor', '!=', 'N/A NO NEED TO IMPORT'); + // // }) + // ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + // $query->payments() + // ->complete() + // ->whereBetween('created_at', [$startDate, $endDate]) + // ->latest('created_at'); + // }) + // ->whereDoesntHave('transactions', function ($query) use ($startDate, $endDate) { + // $query->payments() + // ->complete() + // ->where(function ($q) use ($startDate, $endDate) { + // $q->where('created_at', '<', $startDate) + // ->orWhere('created_at', '>', $endDate); + // }); + // }) + // ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + // $query->where('type', TransactionType::INVOICE) + // ->latest('created_at') + // ->whereDoesntHave('attributesKVP', function ($invoiceQuery) { + // $invoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + // }); + // }) + // ->whereDoesntHave('attributesKVP', function ($invoiceQuery) { + // $invoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + // }); + + return $builder->whereIn('status', [ + ApprovalStatus::APPROVED, + ApprovalStatus::COMPLETED, + ApprovalStatus::SUSPENDED, + ]) + ->where(function ($q) use ($startDate, $endDate) { + $q->where(function ($q) { + $q->whereDoesntHave('company', function ($query) { + $query->where('debtor', 'N/A NO NEED TO IMPORT'); + }); + }) + // Bookings without refunds + ->where(function ($q1) use ($startDate, $endDate) { + $q1->where('status', ApprovalStatus::COMPLETED) + ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + $query->payments() + ->complete() + ->whereBetween('created_at', [$startDate, $endDate]); + }) + ->whereDoesntHave('transactions', function ($query) { + $query->payments() + ->complete() + ->where(function ($q) { + $q->where('created_at', '<', '2025-07-01 00:00:00'); + }); + }) + ->where(function ($query){ + $query + ->whereDoesntHave('transactions.attributesKVP', function ($subQuery) { + $subQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }) + ->whereDoesntHave('attributesKVP', function ($subQuery) { + $subQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }); + }); + }) + // OR bookings with refunds + ->orWhere(function ($q2) use ($startDate, $endDate) { + $q2->whereHas('transactions', function ($query) use ($startDate, $endDate) { + $query->payments() + ->whereBetween('created_at', [$startDate, $endDate]) + ->whereHas('transactions', function ($refundQuery) { + $refundQuery->refunds() + ->whereIn('status', [ApprovalStatus::APPROVED]); + }); + }) + ->whereDoesntHave('transactions', function ($query) { + $query->payments() + ->complete() + ->where(function ($q) { + $q->where('created_at', '<', '2025-07-01 00:00:00'); + }); + }) + ->where(function ($query){ + $query + ->whereDoesntHave('transactions.attributesKVP', function ($subQuery) { + $subQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }) + + ->whereDoesntHave('attributesKVP', function ($subQuery) { + $subQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }); + }); + }); + }) + ->with([ + 'company', + 'transactions.transactionDetails', + 'transactions.voucherRedemption', + ]); + } +} diff --git a/app/Classes/General/Eloquent/Filters/DateRangeWithData.php b/app/Classes/General/Eloquent/Filters/DateRangeWithData.php new file mode 100644 index 00000000..fa2e5161 --- /dev/null +++ b/app/Classes/General/Eloquent/Filters/DateRangeWithData.php @@ -0,0 +1,151 @@ +startOfDay() + : Carbon::now()->subMonth()->startOfDay(); + + $endDate = isset($dt2) && $dt2 + ? Carbon::createFromFormat('Y-m-d', $dt2)->endOfDay() + : Carbon::now()->endOfDay(); + + $minAllowedDate = Carbon::createFromFormat('Y-m-d', '2025-07-01')->startOfDay(); + if ($startDate->lt($minAllowedDate) || $endDate->lt($minAllowedDate)) { + $startDate = Carbon::createFromFormat('Y-m-d', '1970-01-01')->startOfDay(); + $endDate = Carbon::createFromFormat('Y-m-d', '1970-01-01')->startOfDay(); + } + + // return $builder->with([ + // 'company', + // 'transactions.transactionDetails', + // 'transactions.voucherRedemption', + // ]) + + // // ->whereHas('company', function ($query) { + // // $query->where('debtor', '!=', 'N/A NO NEED TO IMPORT'); + // // }) + // ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + // $query->payments() + // ->complete() + // ->whereBetween('created_at', [$startDate, $endDate]) + // ->latest('created_at'); + // }) + // ->whereDoesntHave('transactions', function ($query) use ($startDate, $endDate) { + // $query->payments() + // ->complete() + // ->where(function ($q) use ($startDate, $endDate) { + // $q->where('created_at', '<', $startDate) + // ->orWhere('created_at', '>', $endDate); + // }); + // }) + // ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + // $query->where('type', TransactionType::INVOICE) + // ->latest('created_at') + // ->whereHas('attributesKVP', function ($invoiceQuery) { + // $invoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + // }); + // }); + // // ->WhereHas('attributesKVP', function ($invoiceQuery) { + // // $invoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + // // }); + + + return $builder->whereIn('status', [ + ApprovalStatus::APPROVED, + ApprovalStatus::COMPLETED, + ApprovalStatus::SUSPENDED, + ]) + ->where(function ($q) use ($startDate, $endDate) { + $q->where(function ($q) { + $q->whereDoesntHave('company', function ($query) { + $query->where('debtor', 'N/A NO NEED TO IMPORT'); + }); + }) + // Bookings without refunds + ->where(function ($q1) use ($startDate, $endDate) { + $q1->where('status', ApprovalStatus::COMPLETED) + ->whereHas('transactions', function ($query) use ($startDate, $endDate){ + $query->payments() + ->complete() + ->whereBetween('created_at', [$startDate, $endDate]); + }) + ->whereDoesntHave('transactions', function ($query) { + $query->payments() + ->complete() + ->where(function ($q) { + $q->where('created_at', '<', '2025-07-01 00:00:00'); + }); + }) + ->where(function ($subQuery){ + $subQuery + ->whereHas('transactions', function ($query) { + $query->where('type', TransactionType::INVOICE) + ->whereHas('attributesKVP', function ($invoiceQuery) { + $invoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }); + }) + ->orWhereHas('attributesKVP', function ($noInvoiceQuery) { + $noInvoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }); + }); + }) + // OR bookings with refunds + ->orWhere(function ($q2) use ($startDate, $endDate) { + $q2->whereHas('transactions', function ($query) use ($startDate, $endDate) { + $query->payments() + ->whereBetween('created_at', [$startDate, $endDate]) + ->whereHas('transactions', function ($refundQuery) { + $refundQuery->refunds() + ->whereIn('status', [ApprovalStatus::APPROVED]); + }); + }) + ->whereDoesntHave('transactions', function ($query) { + $query->payments() + ->complete() + ->where(function ($q) { + $q->where('created_at', '<', '2025-07-01 00:00:00'); + }); + }) + ->where(function ($subQuery){ + $subQuery + ->whereHas('transactions', function ($query) { + $query->where('type', TransactionType::INVOICE) + ->whereHas('attributesKVP', function ($invoiceQuery) { + $invoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }); + }) + ->orWhereHas('attributesKVP', function ($noInvoiceQuery) { + $noInvoiceQuery->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE); + }); + }); + }); + }) + ->with([ + 'company', + 'transactions.transactionDetails', + 'transactions.voucherRedemption', + ]); + } +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/ListBookingsSalesInvoiceLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/ListBookingsSalesInvoiceLogic.php new file mode 100644 index 00000000..d8847d2d --- /dev/null +++ b/app/Classes/Modules/Bookings/ControllersLogic/ListBookingsSalesInvoiceLogic.php @@ -0,0 +1,108 @@ + 'Retrieved Bookings with Sales Invoice', + 'message' => 'You have successfully retrieved a list of Bookings with Sales Invoice' + ]; + } + + /** @var CanListBookings */ + private $canListBookings; + + /** @var ListsBookings */ + private $listsBookings; + + /** + * ListBookingsSalesInvoiceLogic constructor. + * @param CanListBookings $canListBookings + * @param ListsBookings $listsBookings + */ + public function __construct(CanListBookings $canListBookings, ListsBookings $listsBookings) + { + $this->canListBookings = $canListBookings; + $this->listsBookings = $listsBookings; + } + + public function logic(Request $request) : JsonResponse + { + // $this->canListBookings->passes(); //cief todo: 90 - apipub + + $filters = $request->input('filters'); + if (is_string($filters)) { + $filters = json_decode($filters, true); + } + + if (!is_array($filters)) { + return response()->json([ + 'message' => 'Invalid filters format. Expected JSON object.', + 'payload' => new \stdClass(), + ], 400); + } + + // Only these filters are allowed + $allowedKeys = ['per_page', 'status', 'order_by', 'date_range', 'date_range_no_data', 'date_range_with_data']; + + $extra = array_diff(array_keys($filters), $allowedKeys); + if (!empty($extra)) { + return response()->json([ + 'message' => 'Invalid filter(s) provided: ' . implode(', ', $extra), + 'payload' => new \stdClass(), + ], 400); + } + + // Required filters that must always exist + $alwaysRequired = ['per_page', 'status']; + + $missing = array_diff($alwaysRequired, array_keys($filters)); + if (!empty($missing)) { + return response()->json([ + 'message' => 'Missing required filter(s): ' . implode(', ', $missing), + 'payload' => new \stdClass(), + ], 400); + } + + if ((int) $filters['status'] !== 3) { + return response()->json([ + 'message' => 'Invalid status value.', + 'payload' => new \stdClass(), + ], 400); + } + + // Required at least one of: date_range or date_range_no_data + if (!isset($filters['date_range']) && !isset($filters['date_range_no_data']) && !isset($filters['date_range_with_data'])) { + return response()->json([ + 'message' => 'Either "date_range" or "date_range_no_data" or "date_range_with_data" filter is required.', + 'payload' => new \stdClass(), + ], 400); + } + $filters = $request->input('filters'); + if (is_string($filters)) { + $decoded = json_decode($filters, true) ?? []; + unset($decoded['status']); + $decoded['order_by'] = ['column' => 'id', 'DESC' => false]; + $request->merge(['filters' => json_encode($decoded)]); + } + + $query = $this->listsBookings->execute($this->listsBookings->deserializeFilters($request->input('filters'))); + + return $this->collectionResponse(AutocountSalesInvoiceBookingResource::collection($query)); + + } +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/UpdateBatchBookingSalesInvoiceLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/UpdateBatchBookingSalesInvoiceLogic.php new file mode 100644 index 00000000..55a863eb --- /dev/null +++ b/app/Classes/Modules/Bookings/ControllersLogic/UpdateBatchBookingSalesInvoiceLogic.php @@ -0,0 +1,93 @@ + 'Updated Bookings with Sales Invoices', + 'message' => 'You have successfully updated multiple bookings with Sales Invoices', + ]; + } + + /** @var CanUpdateBookingSalesInvoice */ + private $canUpdateBookingSalesInvoice; + + /** + * UpdateBatchBookingSalesInvoiceLogic constructor. + * @param CanUpdateBookingSalesInvoice $canUpdateBookingSalesInvoice + */ + public function __construct( + CanUpdateBookingSalesInvoice $canUpdateBookingSalesInvoice + ) { + $this->canUpdateBookingSalesInvoice = $canUpdateBookingSalesInvoice; + } + + /** + * Handles batch update for multiple booking sales invoices + * + * @param Request $request + * @return JsonResponse + * @throws ErrorException + */ + public function logic(Request $request): JsonResponse + { + $collections = $request->input('collections', []); + + if (!is_array($collections) || empty($collections)) { + return response()->json([ + 'error' => 'Invalid or empty collections array provided.' + ], 400); + } + + $jobs = []; + $processed = []; + + foreach ($collections as $index => $bookingData) { + try { + $data = [ + 'docno' => $bookingData['docno'] ?? $bookingData['DocNo'] ?? null, + 'docdate' => $bookingData['docdate'] ?? $bookingData['DocDate'] ?? null, + 'debtorcode' => $bookingData['debtorcode'] ?? $bookingData['DebtorCode'] ?? null, + 'ref' => $bookingData['ref'] ?? $bookingData['Ref'] ?? null, + 'shipinfo' => $bookingData['shipinfo'] ?? $bookingData['ShipInfo'] ?? null, + 'accno' => $bookingData['accno'] ?? $bookingData['AccNo'] ?? null, + 'detaildescription' => $bookingData['detaildescription'] ?? $bookingData['DetailDescription'] ?? null, + 'furtherdescription' => $bookingData['furtherdescription'] ?? $bookingData['FurtherDescription'] ?? null, + 'classification' => $bookingData['classification'] ?? $bookingData['Classification'] ?? null, + 'deptno' => $bookingData['deptno'] ?? $bookingData['DeptNo'] ?? null, + 'qty' => $bookingData['qty'] ?? $bookingData['Qty'] ?? null, + 'unitprice' => $bookingData['unitprice'] ?? $bookingData['UnitPrice'] ?? null, + 'submiteinvoice' => $bookingData['submiteinvoice'] ?? $bookingData['SubmitEinvoice'] ?? null, + 'consolidatedeinvoice' => $bookingData['consolidatedeinvoice'] ?? $bookingData['ConsolidatedEinvoice'] ?? null, + 'einvoicevalidationlink' => $bookingData['einvoicevalidationlink'] ?? $bookingData['EInvoiceValidationLink'] ?? null, + ]; + + $dto = new UpdateBookingSalesInvoiceDTO($data); + $this->canUpdateBookingSalesInvoice->passes($dto); + + ProcessSalesInvoiceReportV2CommandJob::dispatch($dto->toArray()); + + } catch (\Throwable $e) { + Log::error("Failed to process booking at index {$index}: " . $e->getMessage()); + } + } + + return $this->response([]); + } +} diff --git a/app/Classes/Modules/Bookings/ControllersLogic/UpdateBookingSalesInvoiceLogic.php b/app/Classes/Modules/Bookings/ControllersLogic/UpdateBookingSalesInvoiceLogic.php new file mode 100644 index 00000000..ef604c2c --- /dev/null +++ b/app/Classes/Modules/Bookings/ControllersLogic/UpdateBookingSalesInvoiceLogic.php @@ -0,0 +1,74 @@ + 'Updated Booking with Sales Invoice', + 'message' => 'You have successfully updated Booking with Sales Invoice' + ]; + } + + /** @var CanUpdateBookingSalesInvoice */ + private $canUpdateBookingSalesInvoice; + + + /** + * UpdateBookingSalesInvoiceLogic constructor. + * @param CanUpdateBookingSalesInvoice $canUpdateBookingSalesInvoice + */ + public function __construct( + CanUpdateBookingSalesInvoice $canUpdateBookingSalesInvoice + ) + { + $this->canUpdateBookingSalesInvoice = $canUpdateBookingSalesInvoice; + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ErrorException + */ + public function logic(Request $request) : JsonResponse + { + $data = [ + 'docno' => $request->input('docno', $request->input('DocNo')), + 'docdate' => $request->input('docdate', $request->input('DocDate')), + 'debtorcode' => $request->input('debtorcode', $request->input('DebtorCode')), + 'ref' => $request->input('ref', $request->input('Ref')), + 'shipinfo' => $request->input('shipinfo', $request->input('ShipInfo')), + 'accno' => $request->input('accno', $request->input('AccNo')), + 'detaildescription' => $request->input('detaildescription', $request->input('DetailDescription')), + 'furtherdescription' => $request->input('furtherdescription', $request->input('FurtherDescription')), + 'classification' => $request->input('classification', $request->input('Classification')), + 'deptno' => $request->input('deptno', $request->input('DeptNo')), + 'qty' => $request->input('qty', $request->input('Qty')), + 'unitprice' => $request->input('unitprice', $request->input('UnitPrice')), + 'submiteinvoice' => $request->input('submiteinvoice', $request->input('SubmitEinvoice')), + 'consolidatedeinvoice' => $request->input('consolidatedeinvoice', $request->input('ConsolidatedEinvoice')), + 'einvoicevalidationlink' => $request->input('einvoicevalidationlink', $request->input('EInvoiceValidationLink')), + ]; + + $dto = new UpdateBookingSalesInvoiceDTO($data); + $this->canUpdateBookingSalesInvoice->passes($dto); + + ProcessSalesInvoiceReportV2CommandJob::dispatch($dto->toArray()); + + return $this->response([]); + } +} diff --git a/app/Classes/Modules/Bookings/DataTransferObjects/UpdateBookingSalesInvoiceDTO.php b/app/Classes/Modules/Bookings/DataTransferObjects/UpdateBookingSalesInvoiceDTO.php new file mode 100644 index 00000000..096749f2 --- /dev/null +++ b/app/Classes/Modules/Bookings/DataTransferObjects/UpdateBookingSalesInvoiceDTO.php @@ -0,0 +1,184 @@ +docNo = $data['docno']; + $this->docDate = $data['docdate']; + $this->debtorCode = $data['debtorcode']; + $this->ref = $data['ref'] ?? null; + $this->shipInfo = $data['shipinfo'] ?? null; + $this->accNo = $data['accno'] ?? null; + $this->detailDescription = $data['detaildescription'] ?? null; + $this->furtherDescription = $data['furtherdescription'] ?? null; + $this->classification = $data['classification'] ?? null; + $this->deptNo = $data['deptno'] ?? null; + $this->qty = (float) ($data['qty'] ?? 0); + $this->unitPrice = (float) ($data['unitprice'] ?? 0); + $this->submitEinvoice = filter_var($data['submiteinvoice'] ?? false, FILTER_VALIDATE_BOOLEAN); + $this->consolidatedEinvoice = filter_var($data['consolidatedeinvoice'] ?? false, FILTER_VALIDATE_BOOLEAN); + $this->einvoiceValidationLink = $data['einvoicevalidationlink'] ?? null; + } + + /** + * @return string|null + */ + public function getDocNo(): ?string + { + return $this->docNo; + } + + /** + * @return string|null + */ + public function getDocDate(): ?string + { + return $this->docDate; + } + + /** + * @return string|null + */ + public function getDebtorCode(): ?string + { + return $this->debtorCode; + } + + /** + * @return string|null + */ + public function getRef(): ?string + { + return $this->ref; + } + + /** + * @return string|null + */ + public function getShipInfo(): ?string + { + return $this->shipInfo; + } + + /** + * @return string|null + */ + public function getAccNo(): ?string + { + return $this->accNo; + } + + /** + * @return string|null + */ + public function getDetailDescription(): ?string + { + return $this->detailDescription; + } + + /** + * @return string|null + */ + public function getFurtherDescription(): ?string + { + return $this->furtherDescription; + } + + /** + * @return string|null + */ + public function getClassification(): ?string + { + return $this->classification; + } + + /** + * @return string|null + */ + public function getDeptNo(): ?string + { + return $this->deptNo; + } + + /** + * @return float + */ + public function getQty(): float + { + return $this->qty; + } + + /** + * @return float + */ + public function getUnitPrice(): float + { + return $this->unitPrice; + } + + /** + * @return bool + */ + public function getSubmitEinvoice(): bool + { + return $this->submitEinvoice; + } + + /** + * @return bool + */ + public function getConsolidatedEinvoice(): bool + { + return $this->consolidatedEinvoice; + } + + /** + * @return string|null + */ + public function getEinvoiceValidationLink(): ?string + { + return $this->einvoiceValidationLink; + } + + public function toArray(): array + { + return [ + 'docno' => $this->docNo, + 'docdate' => $this->docDate, + 'debtorcode' => $this->debtorCode, + 'ref' => $this->ref, + 'shipinfo' => $this->shipInfo, + 'accno' => $this->accNo, + 'detaildescription' => $this->detailDescription, + 'furtherdescription' => $this->furtherDescription, + 'classification' => $this->classification, + 'deptno' => $this->deptNo, + 'qty' => $this->qty, + 'unitprice' => $this->unitPrice, + 'submiteinvoice' => $this->submitEinvoice, + 'consolidatedeinvoice' => $this->consolidatedEinvoice, + 'einvoicevalidationlink' => $this->einvoiceValidationLink, + ]; + } +} diff --git a/app/Classes/Modules/Bookings/Standards/Rules/CanListBookingsSalesInvoice.php b/app/Classes/Modules/Bookings/Standards/Rules/CanListBookingsSalesInvoice.php new file mode 100644 index 00000000..b2f409da --- /dev/null +++ b/app/Classes/Modules/Bookings/Standards/Rules/CanListBookingsSalesInvoice.php @@ -0,0 +1,40 @@ +bookingSalesInvoiceValidation = $bookingSalesInvoiceValidation; + } + + /** + * @return bool + */ + protected function authorized($object): bool + { + return true; + } + + /** + * @param UpdateBookingSalesInvoiceDTO $object + * @return bool + * @throws \App\Classes\Exceptions\RequestValidationException + */ + protected function validators($object): bool + { + return $this->bookingSalesInvoiceValidation->validate($object); + } + + /** + * @param UpdateBookingSalesInvoiceDTO $object + * @return bool + */ + protected function criteria($object): bool + { + return true; + } + +} diff --git a/app/Classes/Modules/Bookings/Standards/Validators/BookingSalesInvoiceValidation.php b/app/Classes/Modules/Bookings/Standards/Validators/BookingSalesInvoiceValidation.php new file mode 100644 index 00000000..b13037ff --- /dev/null +++ b/app/Classes/Modules/Bookings/Standards/Validators/BookingSalesInvoiceValidation.php @@ -0,0 +1,57 @@ + $object->getDocNo(), + 'docdate' => $object->getDocDate(), + 'debtorcode' => $object->getDebtorCode(), + 'ref' => $object->getRef(), + 'shipinfo' => $object->getShipInfo(), + 'accno' => $object->getAccNo(), + 'detaildescription' => $object->getDetailDescription(), + 'furtherdescription' => $object->getFurtherDescription(), + 'classification' => $object->getClassification(), + 'deptno' => $object->getDeptNo(), + 'qty' => $object->getQty(), + 'unitprice' => $object->getUnitPrice(), + 'submiteinvoice' => $object->getSubmitEinvoice(), + 'consolidatedeinvoice' => $object->getConsolidatedEinvoice(), + 'einvoicevalidationlink' => $object->getEinvoiceValidationLink(), + ]; + } + + /** + * @return array + */ + protected function rules(): array + { + return [ + 'docno' => 'required|string', + 'docdate' => 'required|date', + 'debtorcode' => 'required|string', + 'ref' => 'required|string', + 'shipinfo' => 'required|string', + 'einvoicevalidationlink' => 'required|string', + ]; + } + + /** + * @return array + */ + protected function messages(): array + { + return []; + } +} diff --git a/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceReport.php b/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceReport.php index a7c170b7..66254a9d 100644 --- a/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceReport.php +++ b/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceReport.php @@ -82,6 +82,11 @@ class ExportsSalesInvoiceReport implements FromQuery, WithHeadings, WithHeadingR }); } + public function getRecordCount(): int + { + return $this->query()->count(); + } + /** * @param Booking $booking * @return array diff --git a/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceWithRefundReport.php b/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceWithRefundReport.php index c6ac49cf..9f134a04 100644 --- a/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceWithRefundReport.php +++ b/app/Classes/Modules/Exports/Services/ExportsSalesInvoiceWithRefundReport.php @@ -84,6 +84,11 @@ class ExportsSalesInvoiceWithRefundReport implements FromQuery, WithHeadings, Wi }); } + public function getRecordCount(): int + { + return $this->query()->count(); + } + /** * @param Booking $booking * @return array diff --git a/app/Classes/Modules/Transactions/Processors/CreateInvoiceTransactionV2Processor.php b/app/Classes/Modules/Transactions/Processors/CreateInvoiceTransactionV2Processor.php index 230296e4..d4f7d32c 100644 --- a/app/Classes/Modules/Transactions/Processors/CreateInvoiceTransactionV2Processor.php +++ b/app/Classes/Modules/Transactions/Processors/CreateInvoiceTransactionV2Processor.php @@ -111,10 +111,6 @@ class CreateInvoiceTransactionV2Processor // Log::info('CreateInvoiceTransactionV2Processor generateEInvoiceRefund: ' . json_encode($generateEInvoiceRefund)); // Log::info('CreateInvoiceTransactionV2Processor bookingOriginalStatus: ' . json_encode($bookingOriginalStatus)); - if($generateEInvoiceRefund){ - $generateEInvoice = true; //With or without refund, these 2 flags are meant to Generate E-Invoice, so cannot have opposite indicator - } - if ($booking->status === ApprovalStatus::COMPLETED && !$generateEInvoiceRefund) { Log::info('CreateInvoiceTransactionV2Processor Check 1 Bypass New Business Logic Update for booking ' . $booking->id); // return; @@ -124,7 +120,7 @@ class CreateInvoiceTransactionV2Processor $refund_amount = $this->calculatesBookingRefundAmount->execute($booking, $booking->fix_currency_id); $booking_amount = $booking->fix_amount; - if ((float) $booking_amount === (float) $refund_amount && !$generateEInvoice) { + if ((float) $booking_amount === (float) $refund_amount && !$generateEInvoiceRefund) { return; } @@ -257,6 +253,19 @@ class CreateInvoiceTransactionV2Processor $voucherRedemption = $transaction->voucherRedemption; + if($generateEInvoiceRefund){ + $metadata = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE)->first(); + if($metadata){ + $generateEInvoice = true; + } + else{ + $metadata = $transaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE)->first(); + if($metadata){ + $generateEInvoice = true; + } + } + } + if($purchaseOrder){ // purchase order if(!$generateEInvoiceRefund){ diff --git a/app/Classes/Modules/Transactions/Processors/UpdateRefundTransactionStatusProcessor.php b/app/Classes/Modules/Transactions/Processors/UpdateRefundTransactionStatusProcessor.php index 15e7c0a5..f0aeb939 100644 --- a/app/Classes/Modules/Transactions/Processors/UpdateRefundTransactionStatusProcessor.php +++ b/app/Classes/Modules/Transactions/Processors/UpdateRefundTransactionStatusProcessor.php @@ -46,6 +46,9 @@ class UpdateRefundTransactionStatusProcessor /** @var CanUpdateRefundTransactionStatus */ private $canUpdateRefundTransactionStatus; + /** @var CreateInvoiceTransactionV2Processor */ + private $createInvoiceTransactionV2Processor; + /** * UpdateRefundTransactionStatusProcessor constructor. * @param FetchesCompany $fetchesCompany @@ -57,8 +60,9 @@ class UpdateRefundTransactionStatusProcessor * @param CalculatesBookingRefundAmount $calculatesBookingRefundAmount * @param UpdateBookingAmountLogic $updateBookingAmountLogic * @param CanUpdateRefundTransactionStatus $canUpdateRefundTransactionStatus + * @param CreateInvoiceTransactionV2Processor $createInvoiceTransactionV2Processor */ - public function __construct(FetchesCompany $fetchesCompany, FetchesTransaction $fetchesTransaction, UpdatesTransactionStatus $updatesTransactionStatus, DeletesDocument $deletesDocument, CreditWalletProcessor $creditWalletProcessor, CalculatesBookingPayableAmount $calculatesBookingPayableAmount, CalculatesBookingRefundAmount $calculatesBookingRefundAmount, UpdateBookingAmountLogic $updateBookingAmountLogic, CanUpdateRefundTransactionStatus $canUpdateRefundTransactionStatus) + public function __construct(FetchesCompany $fetchesCompany, FetchesTransaction $fetchesTransaction, UpdatesTransactionStatus $updatesTransactionStatus, DeletesDocument $deletesDocument, CreditWalletProcessor $creditWalletProcessor, CalculatesBookingPayableAmount $calculatesBookingPayableAmount, CalculatesBookingRefundAmount $calculatesBookingRefundAmount, UpdateBookingAmountLogic $updateBookingAmountLogic, CanUpdateRefundTransactionStatus $canUpdateRefundTransactionStatus, CreateInvoiceTransactionV2Processor $createInvoiceTransactionV2Processor) { $this->fetchesCompany = $fetchesCompany; $this->fetchesTransaction = $fetchesTransaction; @@ -69,6 +73,7 @@ class UpdateRefundTransactionStatusProcessor $this->calculatesBookingRefundAmount = $calculatesBookingRefundAmount; $this->updateBookingAmountLogic = $updateBookingAmountLogic; $this->canUpdateRefundTransactionStatus = $canUpdateRefundTransactionStatus; + $this->createInvoiceTransactionV2Processor = $createInvoiceTransactionV2Processor; } @@ -125,5 +130,15 @@ class UpdateRefundTransactionStatusProcessor if (!$paidAmount > 0) { $this->updatesTransactionStatus->execute($paymentTransaction, ApprovalStatus::REFUNDED); } + + //A full refund will need invoice generated + if($refundAmount === $booking->fix_amount){ + $this->createInvoiceTransactionV2Processor->execute($booking, "", null, [ + 'generateEInvoice' => false, + 'generateEInvoiceWithNormalInvoiceTemplate' => false, + 'generateEInvoiceRefund' => true, + 'bookingOriginalStatus' => $booking->status + ]); + } } } diff --git a/app/Http/Controllers/Bookings/ListBookingsSalesInvoiceController.php b/app/Http/Controllers/Bookings/ListBookingsSalesInvoiceController.php new file mode 100644 index 00000000..249278af --- /dev/null +++ b/app/Http/Controllers/Bookings/ListBookingsSalesInvoiceController.php @@ -0,0 +1,20 @@ +execute($request); + } +} diff --git a/app/Http/Controllers/Bookings/UpdateBookingSalesInvoiceController.php b/app/Http/Controllers/Bookings/UpdateBookingSalesInvoiceController.php new file mode 100644 index 00000000..6339f464 --- /dev/null +++ b/app/Http/Controllers/Bookings/UpdateBookingSalesInvoiceController.php @@ -0,0 +1,29 @@ +execute($request); + } + + /** + * @param Request $request + * @return JsonResponse + */ + public function updateBatch(Request $request, UpdateBatchBookingSalesInvoiceLogic $logic) : JsonResponse { + return $logic->execute($request); + } +} diff --git a/app/Http/Resources/AutocountSalesInvoiceBookingDetailsResource.php b/app/Http/Resources/AutocountSalesInvoiceBookingDetailsResource.php new file mode 100644 index 00000000..e4069d2f --- /dev/null +++ b/app/Http/Resources/AutocountSalesInvoiceBookingDetailsResource.php @@ -0,0 +1,36 @@ + $this['DocNo'], + 'DocDate' => $this['DocDate'], + 'DebtorCode' => $this['DebtorCode'], + 'Ref' => $this['Ref'], + 'ShipInfo' => $this['ShipInfo'], + 'AccNo' => $this['AccNo'], + 'DetailDescription' => $this['DetailDescription'], + 'FurtherDescription' => $this['FurtherDescription'], + 'Classification' => $this['Classification'], + 'DeptNo' => $this['DeptNo'], + 'Qty' => $this['Qty'], + 'UnitPrice' => $this['UnitPrice'], + 'SubmitEinvoice' => $this['SubmitEinvoice'], + 'ConsolidatedEinvoice' => $this['ConsolidatedEinvoice'], + 'InvoiceStatus' => $this['InvoiceStatus'], + ]; + } + +} diff --git a/app/Http/Resources/AutocountSalesInvoiceBookingResource.php b/app/Http/Resources/AutocountSalesInvoiceBookingResource.php new file mode 100644 index 00000000..83f41170 --- /dev/null +++ b/app/Http/Resources/AutocountSalesInvoiceBookingResource.php @@ -0,0 +1,247 @@ +transactions->where('type', TransactionType::PURCHASE_ORDER)->first(); + $company = $this->company; + + $lastPaymentTransaction = $this->transactions + ->where('type', TransactionType::PAYMENT) + ->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED]) + ->sortByDesc('created_at') + ->first(); + + if(!$lastPaymentTransaction){ + $lastPaymentTransaction = $this->transactions()->payments()->whereIn('status', [ApprovalStatus::COMPLETED, ApprovalStatus::REFUNDED])->latest()->first(); + } + $documentDate = Carbon::parse($lastPaymentTransaction->created_at); + + // if ($documentDate < $this->startDate || $documentDate > $this->endDate) { + // return []; + // } + + if($company->e_invoice === 1){ + $documentDate = $documentDate->copy()->endOfMonth(); + } + + $invoiceTransaction = $this->transactions->where('type', TransactionType::INVOICE)->whereIn('status', [ApprovalStatus::APPROVED, ApprovalStatus::COMPLETED])->first(); + + $currencyId = $this->fix_currency_id; + + $subtotal = 0; + $displayedSubtotal = 0; + $totalPayment = 0; + $averageCurrencyRate = $invoiceTransaction ? $invoiceTransaction->currency_rate : 0; + $paymentSum = $this->transactions + ->where('type', TransactionType::PAYMENT) + ->where('status', ApprovalStatus::COMPLETED) + ->sum(function ($transaction) { + return round($transaction->amount, 2); + }); + if ($paymentSum){ + $averageCurrencyRate = $this->transactions + ->where('type', TransactionType::PAYMENT) + ->where('status', ApprovalStatus::COMPLETED) + ->sum(function ($transaction) { + return $transaction->currency_rate; + }) / $this->transactions + ->where('type', TransactionType::PAYMENT) + ->where('status', ApprovalStatus::COMPLETED) + ->count(); + + $booking = Booking::where('id', $this->id)->first(); + $refundedAmount = (App()->make(CalculatesBookingRefundAmount::class))->execute($booking, 1); + $refundedServiceCharge = (App()->make(CalculatesBookingRefundServiceCharge::class))->execute($booking, 1); + + $totalPayment = $paymentSum - $refundedAmount - $refundedServiceCharge; + } + + $formattedDocumentDate = Carbon::parse($documentDate)->format('m/d/Y'); + + $docNo = '<>'; + $firstItem = true; + if($invoiceTransaction){ + $invoiceTransactionKVP = $invoiceTransaction->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE)->first(); + if($invoiceTransactionKVP){ + $docNo = $invoiceTransactionKVP->value; + } + else { + $bookingKVP = $booking->attributesKVP()->where('key', KVPKey::AUTOCOUNT_DOCNO_INVOICE)->first(); + $docNo = $bookingKVP ? $bookingKVP->value :'<>'; + } + } + + $transactionDetails = $purchaseOrder->transactionDetails; + foreach ($transactionDetails as $detail) { + $displayUnitPrice = 0; + if($averageCurrencyRate && $currencyId){ + $exactUnitPrice = ($currencyId) === 1 ? $detail->price : bcdiv($detail->price, $averageCurrencyRate, 7); + $displayUnitPrice = round($exactUnitPrice, 2); + $itemTotal = bcmul($exactUnitPrice, $detail->quantity, 5); + $displayedItemTotal = round(bcmul($displayUnitPrice, $detail->quantity, 7), 2); + $displayedSubtotal = bcadd($displayedSubtotal, $displayedItemTotal, 2); + $subtotal = bcadd($subtotal, $itemTotal, 5); + } + + $detail = [ + 'DocNo' => $firstItem ? $docNo : '', + 'DocDate' => $formattedDocumentDate, + 'DebtorCode' => $company->debtor, + 'Ref' => $invoiceTransaction ? $invoiceTransaction->bill_no : '', + 'ShipInfo' => $this->marking, + 'AccNo' => '500-0000', + 'DetailDescription' => 'PRODUCT NAME :', + 'FurtherDescription' => $detail->product_name, + 'Classification' => '022', + 'DeptNo' => 'C', + 'Qty' => number_format($detail->quantity, 0), + 'UnitPrice' => $displayUnitPrice ? number_format($displayUnitPrice, 2) : '0', + 'SubmitEinvoice' => $firstItem ? 'T' : '', + 'ConsolidatedEinvoice' => $firstItem ? ($company->e_invoice ? 'F' : 'T') : '', + 'InvoiceStatus' => (string) $this->invoice_status, + ]; + $details->push($detail); + + if($firstItem) { + $firstItem = false; + } + } + + // Voucherify - Starts + $voucherRedemption = $lastPaymentTransaction->voucherRedemption; + if($voucherRedemption){ + $voucherDiscount = $voucherRedemption ? bcmul((string)$voucherRedemption->value, "-1", 2) : "0"; + $voucher = $voucherRedemption->voucher; + $category = $voucher->campaign ? $voucher->campaign->category : null; + $detail = [ + 'DocNo' => '', + 'DocDate' => $formattedDocumentDate, + 'DebtorCode' => $company->debtor, + 'Ref' => $invoiceTransaction ? $invoiceTransaction->bill_no : '', + 'ShipInfo' => $this->marking, + 'AccNo' => $category === 'Compensation Voucher' ? '1000-000' : '949-2000', + 'DetailDescription' => 'PRODUCT NAME :', + 'FurtherDescription' => $voucher->code, + 'Classification' => '022', + 'DeptNo' => 'C', + 'Qty' => '1', + 'UnitPrice' => $voucherDiscount ? number_format($voucherDiscount, 2): '0', + 'SubmitEinvoice' => '', + 'ConsolidatedEinvoice' => '', + 'InvoiceStatus' => (string) $this->invoice_status, + ]; + $details->push($detail); + } + // Voucherify - Ends + + // Service Charge - Starts + $serviceCharge = 0; + if (!$totalPayment && $invoiceTransaction) { + $serviceCharge = $invoiceTransaction->service_charge; + } + else { + $serviceCharge = $this->transactions + ->where('type', TransactionType::PAYMENT) + ->where('status', ApprovalStatus::COMPLETED) + ->sum(function ($transaction) { + return $transaction->service_charge; + }); + } + + $detail = [ + 'DocNo' => '', + 'DocDate' => $formattedDocumentDate, + 'DebtorCode' => $company->debtor, + 'Ref' => $invoiceTransaction ? $invoiceTransaction->bill_no : '', + 'ShipInfo' => $this->marking, + 'AccNo' => '500-0000', + 'DetailDescription' => 'PRODUCT NAME :', + 'FurtherDescription' => 'Service Charge', + 'Classification' => '022', + 'DeptNo' => 'C', + 'Qty' => '1', + 'UnitPrice' => $serviceCharge ? number_format($serviceCharge, 2) : '0', + 'SubmitEinvoice' => '', + 'ConsolidatedEinvoice' => '', + 'InvoiceStatus' => (string) $this->invoice_status, + ]; + $details->push($detail); + // Service Charge - Ends + + // Adjustment - Starts + if($invoiceTransaction){ + $adjustment = 0; + $voucherRedemption = $invoiceTransaction->voucherRedemption; + $voucherDiscount = $voucherRedemption ? bcmul((string)$voucherRedemption->value, "-1", 2) : "0"; + + $displayedSubtotal = is_numeric($displayedSubtotal) ? sprintf('%F', $displayedSubtotal) : '0'; + $serviceCharge = is_numeric($serviceCharge) ? sprintf('%F', $serviceCharge) : '0'; + $tax = is_numeric($invoiceTransaction->tax) ? sprintf('%F', $invoiceTransaction->tax) : '0'; + $voucherDiscount = is_numeric($voucherDiscount) ? sprintf('%F', $voucherDiscount) : '0'; + + $displayedTotal = bcadd( + bcadd( + bcadd($displayedSubtotal, $serviceCharge, 5), + $tax, + 5 + ), + $voucherDiscount, + 5 + ); + + $expectedTotal = bcadd(bcadd(bcadd($subtotal, $serviceCharge, 5), $invoiceTransaction->tax, 5), $voucherDiscount, 5); + $adjustment = bcsub($expectedTotal, $displayedTotal, 5); + + if ($totalPayment) { + $expectedTotal = $totalPayment; + $adjustment = bcsub($expectedTotal, $displayedTotal, 5); + } + + $detail = [ + 'DocNo' => '', + 'DocDate' => $formattedDocumentDate, + 'DebtorCode' => $company->debtor, + 'Ref' => $invoiceTransaction->bill_no, + 'ShipInfo' => $this->marking, + 'AccNo' => '500-0000', + 'DetailDescription' => 'PRODUCT NAME :', + 'FurtherDescription' => 'Adjustment', + 'Classification' => '022', + 'DeptNo' => 'C', + 'Qty' => '1', + 'UnitPrice' => $adjustment ? number_format($adjustment, 2) : '0', + 'SubmitEinvoice' => '', + 'ConsolidatedEinvoice' => '', + 'InvoiceStatus' => (string) $this->invoice_status, + ]; + $details->push($detail); + } + // Adjustment - Ends + + return AutocountSalesInvoiceBookingDetailsResource::collection( + collect($details) + ); + } +} diff --git a/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue b/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue index 35281a85..5bce1293 100644 --- a/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue +++ b/resources/assets/vue/components/bookings/forms/PurchaseOrderFormComponent.vue @@ -195,7 +195,7 @@
-
+
diff --git a/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue b/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue index 8b57a460..a9845bc3 100644 --- a/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue +++ b/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue @@ -133,7 +133,7 @@
-
+
diff --git a/routes/apipub.php b/routes/apipub.php index b2a5e2ce..6e2cdb19 100644 --- a/routes/apipub.php +++ b/routes/apipub.php @@ -1,9 +1,17 @@ 'apipub', 'prefix' => 'v1', 'as' => 'apipub.'], function () { Route::group(['middleware' => 'token.check'], function () { Route::get('transactions/mappable/query', 'Transactions\ListMappableTransactionsController@list')->name('transaction.mappable.list'); + + Route::group(['prefix' => 'booking', 'as' => 'booking.'], function () { + Route::get('/sales-invoice/list', [ListBookingsSalesInvoiceController::class, 'list'])->name('booking.sales-invoice.list'); + Route::post('/sales-invoice', [UpdateBookingSalesInvoiceController::class, 'update'])->name('booking.sales-invoice.update'); + Route::post('/sales-invoice/batch', [UpdateBookingSalesInvoiceController::class, 'updateBatch'])->name('booking.sales-invoice.batch.update'); + }); }); });