Add economic_transfer_executor and economic_transfer_queue classes for handling e-conomic invoice transfer logic, queue management, and processing. Include unit tests for Redis cache validation.
This commit is contained in:
+493
-17
@@ -5992,6 +5992,139 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/collected-invoices/economic:
|
||||
post:
|
||||
tags:
|
||||
- Invoices
|
||||
summary: Queue transfer of collected invoice to e-conomic
|
||||
description: |
|
||||
Queues collected invoice transfer to e-conomic.
|
||||
Processing runs asynchronously and can be tracked through queue status endpoints.
|
||||
operationId: queueCollectedInvoiceEconomicTransfer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [id]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
send_as_is:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
'202':
|
||||
description: Collected invoice transfer queued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/collected-invoices/economic/queue:
|
||||
get:
|
||||
tags:
|
||||
- Invoices
|
||||
summary: List collected-invoice e-conomic transfer queue jobs
|
||||
operationId: listCollectedInvoiceEconomicQueueJobs
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
required: false
|
||||
description: Comma-separated queue statuses to filter by.
|
||||
schema:
|
||||
type: string
|
||||
example: "QUEUED,FAILED"
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 500
|
||||
default: 50
|
||||
- name: offset
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Queue jobs retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueListResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/collected-invoices/economic/queue/status:
|
||||
get:
|
||||
tags:
|
||||
- Invoices
|
||||
summary: Get collected-invoice e-conomic transfer queue job status
|
||||
operationId: getCollectedInvoiceEconomicQueueJobStatus
|
||||
parameters:
|
||||
- name: job_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Queue job status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueStatusResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/collected-invoices/economic/queue/retry:
|
||||
post:
|
||||
tags:
|
||||
- Invoices
|
||||
summary: Retry failed collected-invoice queue job
|
||||
operationId: retryCollectedInvoiceEconomicQueueJob
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [job_id]
|
||||
properties:
|
||||
job_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Queue job retried
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueRetryResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/collected-invoices/economic/compare:
|
||||
get:
|
||||
tags:
|
||||
@@ -7672,39 +7805,175 @@ paths:
|
||||
post:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Export draft invoice to e-conomic
|
||||
description: Export a draft invoice to e-conomic
|
||||
operationId: exportDraftInvoiceToEconomic
|
||||
summary: Queue draft invoice export to e-conomic
|
||||
description: Queue a draft invoice export job for asynchronous processing.
|
||||
operationId: queueDraftInvoiceExportToEconomic
|
||||
requestBody:
|
||||
required: false
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
schema:
|
||||
type: object
|
||||
required: [order_id]
|
||||
properties:
|
||||
order_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Draft invoice exported successfully
|
||||
'202':
|
||||
description: Draft invoice export queued
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/economic/invoice/draft/export/status:
|
||||
get:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Get queued draft export job status
|
||||
operationId: getDraftInvoiceExportQueueStatus
|
||||
parameters:
|
||||
- name: job_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Queue job status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueStatusResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/economic/invoice/draft/export/retry:
|
||||
post:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Retry failed draft export queue job
|
||||
operationId: retryDraftInvoiceExportQueueJob
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [job_id]
|
||||
properties:
|
||||
job_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Queue job retried
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueRetryResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/economic/invoice/export:
|
||||
post:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Export invoice to e-conomic
|
||||
description: Export a booked invoice to e-conomic
|
||||
operationId: exportInvoiceToEconomic
|
||||
summary: Queue invoice export to e-conomic
|
||||
description: Queue booked invoice export job for asynchronous processing.
|
||||
operationId: queueInvoiceExportToEconomic
|
||||
requestBody:
|
||||
required: false
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
schema:
|
||||
type: object
|
||||
required: [order_id]
|
||||
properties:
|
||||
order_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Invoice exported successfully
|
||||
'202':
|
||||
description: Invoice export queued
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/economic/invoice/export/status:
|
||||
get:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Get queued invoice export job status
|
||||
operationId: getInvoiceExportQueueStatus
|
||||
parameters:
|
||||
- name: job_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Queue job status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueStatusResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
/economic/invoice/export/retry:
|
||||
post:
|
||||
tags:
|
||||
- Modules
|
||||
summary: Retry failed invoice export queue job
|
||||
operationId: retryInvoiceExportQueueJob
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [job_id]
|
||||
properties:
|
||||
job_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Queue job retried
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueRetryResponse'
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'500': { $ref: '#/components/responses/InternalServerError' }
|
||||
|
||||
# Module - Stripe Endpoints
|
||||
/modules/stripe/customers:
|
||||
@@ -8135,7 +8404,7 @@ paths:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
example: Failed to execute command: Failed to open property access gate.
|
||||
example: 'Failed to execute command: Failed to open property access gate.'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
@@ -12284,6 +12553,213 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
EconomicTransferQueueStatus:
|
||||
type: string
|
||||
enum:
|
||||
- QUEUED
|
||||
- PROCESSING
|
||||
- COMPLETED
|
||||
- FAILED
|
||||
|
||||
EconomicTransferQueueJob:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
transfer_type:
|
||||
type: string
|
||||
enum:
|
||||
- ORDER_DRAFT_EXPORT
|
||||
- ORDER_INVOICE_EXPORT
|
||||
- COLLECTED_INVOICE_EXPORT
|
||||
status:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueStatus'
|
||||
progress_percent:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
progress_message:
|
||||
type: string
|
||||
nullable: true
|
||||
attempts:
|
||||
type: integer
|
||||
minimum: 0
|
||||
max_attempts:
|
||||
type: integer
|
||||
minimum: 1
|
||||
error_message:
|
||||
type: string
|
||||
nullable: true
|
||||
payload:
|
||||
type: object
|
||||
nullable: true
|
||||
additionalProperties: true
|
||||
result:
|
||||
type: object
|
||||
nullable: true
|
||||
additionalProperties: true
|
||||
created_by:
|
||||
type: integer
|
||||
nullable: true
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
started_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
completed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
next_retry_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
required:
|
||||
- id
|
||||
- transfer_type
|
||||
- status
|
||||
- progress_percent
|
||||
- attempts
|
||||
- max_attempts
|
||||
|
||||
EconomicTransferQueueEnqueueResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
job_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
job:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueJob'
|
||||
required:
|
||||
- message
|
||||
- job_id
|
||||
- job
|
||||
meta:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
includes:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- success
|
||||
- data
|
||||
- meta
|
||||
- includes
|
||||
|
||||
EconomicTransferQueueStatusResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueJob'
|
||||
meta:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
includes:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- success
|
||||
- data
|
||||
- meta
|
||||
- includes
|
||||
|
||||
EconomicTransferQueueRetryResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
job:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueJob'
|
||||
required:
|
||||
- message
|
||||
- job
|
||||
meta:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
includes:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- success
|
||||
- data
|
||||
- meta
|
||||
- includes
|
||||
|
||||
EconomicTransferQueueListResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/EconomicTransferQueueJob'
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
required:
|
||||
- items
|
||||
- count
|
||||
meta:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
includes:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: {}
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
required:
|
||||
- success
|
||||
- data
|
||||
- meta
|
||||
- includes
|
||||
|
||||
CollectedInvoiceEconomicCompareResponse:
|
||||
type: object
|
||||
description: Result of comparing a collected invoice with its E-conomic counterpart
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
use economic_invoice_draft_mo;
|
||||
use Exception;
|
||||
use objects\collected_order_invoices_o;
|
||||
use objects\customer_fixed_pricing_o;
|
||||
use objects\departments_o;
|
||||
use objects\economic_module_orders;
|
||||
use objects\logs_o;
|
||||
use objects\orders_o;
|
||||
use objects\users_o;
|
||||
|
||||
/**
|
||||
* Executes e-conomic transfer flows that are now run by queue workers.
|
||||
*/
|
||||
class economic_transfer_executor
|
||||
{
|
||||
/**
|
||||
* Export order draft invoice to e-conomic.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function exportOrderDraftInvoice(int $order_id, int $user_id = 0): array
|
||||
{
|
||||
$order = (new orders_o())->getOrderById($order_id);
|
||||
if (!$order->exists()) {
|
||||
throw new Exception('Order not found');
|
||||
}
|
||||
|
||||
$order_items = (new orders_o())->getOrderItems($order_id);
|
||||
$order_items = (new orders_o())->applyDepartmentPrices($order_items, $order->department_id->value());
|
||||
if (count($order_items) === 0) {
|
||||
throw new Exception('No order items found');
|
||||
}
|
||||
|
||||
$customer = (new orders_o())->getCustomerByOrderId($order_id);
|
||||
if (!$customer->exists()) {
|
||||
throw new Exception('Customer not found');
|
||||
}
|
||||
|
||||
$customer_economic = $customer->getCustomerEcocomicData()->economic_customer;
|
||||
$economic_invoice_draft = new economic_invoice_draft_mo();
|
||||
$economic_invoice_draft->setCustomerNumber((int)$customer_economic->customer_number);
|
||||
$economic_invoice_draft->setRecipient(
|
||||
$customer_economic->name ?? 'Ukendt',
|
||||
$customer_economic->address ?? 'Ukendt',
|
||||
$customer_economic->zip ?? 'Ukendt',
|
||||
$customer_economic->city ?? 'Ukendt'
|
||||
);
|
||||
|
||||
$department = (new departments_o())->getDepartmentById($order->department_id->value());
|
||||
$this->addTheDepartmentDateReference($economic_invoice_draft, $department['name'], $order);
|
||||
|
||||
$billable_order_items = 0;
|
||||
foreach ($order_items as $order_item) {
|
||||
$added = $this->addOrderItemToInvoice(
|
||||
$customer,
|
||||
$order,
|
||||
$order_item,
|
||||
$economic_invoice_draft,
|
||||
(int)($order_item['quantity'] ?? 1)
|
||||
);
|
||||
if ($added) {
|
||||
$billable_order_items++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($billable_order_items < 1) {
|
||||
throw new Exception('No billable order items found');
|
||||
}
|
||||
|
||||
$result = null;
|
||||
if ($customer->hasOpenInvoiceDraft() && !$customer->invoicePerOrder()) {
|
||||
$open_invoice_draft = (int)$customer->getOpenInvoiceDraft();
|
||||
$result = $this->addOrderToInvoiceDraft($open_invoice_draft, $order, $customer, $order_items);
|
||||
}
|
||||
if ($result === null) {
|
||||
$result = $economic_invoice_draft->createInvoiceDraftExample();
|
||||
}
|
||||
|
||||
if (!isset($result->draftInvoiceNumber) && !isset($result->lines[0])) {
|
||||
(new logs_o())->add(
|
||||
'economic_invoice_draft',
|
||||
'global',
|
||||
3,
|
||||
$user_id,
|
||||
'ECONOMIC_INVOICE_DRAFT_EXPORT',
|
||||
'Failed to create economic invoice draft'
|
||||
);
|
||||
$message = $result->message ?? 'Failed to create economic invoice draft';
|
||||
throw new Exception((string)$message);
|
||||
}
|
||||
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
if ($economic_module_orders->economic_invoice_draft_id->value() > 0) {
|
||||
$economic_invoice_draft->deleteInvoiceDraft($economic_module_orders->economic_invoice_draft_id->value());
|
||||
}
|
||||
|
||||
$new_draft_id = (int)($result->draftInvoiceNumber ?? $customer->getOpenInvoiceDraft());
|
||||
$economic_module_orders->economic_invoice_draft_id->set($new_draft_id);
|
||||
if (!$customer->invoicePerOrder()) {
|
||||
$customer->setOpenInvoiceDraft($new_draft_id);
|
||||
} else {
|
||||
$customer->unsetOpenInvoiceDraft();
|
||||
}
|
||||
|
||||
(new logs_o())->add(
|
||||
'economic_invoice_draft',
|
||||
'global',
|
||||
1,
|
||||
$user_id,
|
||||
'ECONOMIC_INVOICE_DRAFT_EXPORT',
|
||||
'Successfully exported an economic invoice draft'
|
||||
);
|
||||
|
||||
return $economic_module_orders->getArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export booked invoice from existing draft.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function exportOrderInvoice(int $order_id, int $user_id = 0): array
|
||||
{
|
||||
$order = (new orders_o())->getOrderById($order_id);
|
||||
if (!$order->exists()) {
|
||||
throw new Exception('Order not found');
|
||||
}
|
||||
|
||||
$customer = (new orders_o())->getCustomerByOrderId($order_id);
|
||||
if (!$customer->exists()) {
|
||||
throw new Exception('Customer not found');
|
||||
}
|
||||
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
if ($economic_module_orders->economic_invoice_draft_id->value() === 0) {
|
||||
throw new Exception('No economic invoice draft found');
|
||||
}
|
||||
|
||||
$invoice_id = (int)$economic_module_orders->economic_invoice_id->value();
|
||||
if ($invoice_id > 0) {
|
||||
throw new Exception('An invoice has already been created, invoice ID: ' . $invoice_id);
|
||||
}
|
||||
|
||||
$invoice_draft_id = (int)$economic_module_orders->economic_invoice_draft_id->value();
|
||||
$economic_invoice_draft = new economic_invoice_draft_mo();
|
||||
$result = $economic_invoice_draft->publishInvoiceDraft($invoice_draft_id);
|
||||
|
||||
if (!isset($result->bookedInvoiceNumber)) {
|
||||
(new logs_o())->add(
|
||||
'economic_invoice',
|
||||
'global',
|
||||
3,
|
||||
$user_id,
|
||||
'ECONOMIC_INVOICE_EXPORT',
|
||||
'Failed to create economic invoice from draft: ' . $invoice_draft_id
|
||||
);
|
||||
$message = $result->message ?? 'Failed to create economic invoice';
|
||||
throw new Exception((string)$message);
|
||||
}
|
||||
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
$economic_module_orders->economic_invoice_id->set((int)$result->bookedInvoiceNumber);
|
||||
$customer->unsetOpenInvoiceDraft();
|
||||
|
||||
(new logs_o())->add(
|
||||
'economic_invoice',
|
||||
'global',
|
||||
1,
|
||||
$user_id,
|
||||
'ECONOMIC_INVOICE_EXPORT',
|
||||
'Successfully exported an economic invoice'
|
||||
);
|
||||
|
||||
return $economic_module_orders->getArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export collected invoice to e-conomic.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function exportCollectedInvoice(int $collected_invoice_id, bool $send_as_is = false, int $user_id = 0): array
|
||||
{
|
||||
$collected_order_invoices = (new collected_order_invoices_o())->select($collected_invoice_id);
|
||||
$collected_order_invoices->requireSelected();
|
||||
|
||||
if ($collected_order_invoices->external_id->value() === null) {
|
||||
if (!$send_as_is) {
|
||||
$customer_fixed_pricing_o = new customer_fixed_pricing_o();
|
||||
if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
|
||||
$customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
|
||||
$customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
|
||||
$collected_order_invoices->overridePricesFixed($customer_fixed_pricing_price);
|
||||
} else {
|
||||
$collected_order_invoices->addVehicleSubscriptionsTransaction();
|
||||
}
|
||||
} else {
|
||||
$collected_order_invoices->removeSpecialArrangements();
|
||||
$collected_order_invoices->setAllItemsToBeIncludedInInvoice();
|
||||
}
|
||||
|
||||
$collected_order_invoices->addToEconomic();
|
||||
} else {
|
||||
if ($collected_order_invoices->booked_invoice_id->value() !== null) {
|
||||
throw new Exception('Invoice has already been booked');
|
||||
}
|
||||
if ($collected_order_invoices->isDraftExisting()) {
|
||||
throw new Exception('Invoice draft already exists in E-Conomic');
|
||||
}
|
||||
|
||||
$customer_fixed_pricing_o = new customer_fixed_pricing_o();
|
||||
if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
|
||||
$customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
|
||||
$customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
|
||||
$collected_order_invoices->overridePricesFixed($customer_fixed_pricing_price);
|
||||
} else {
|
||||
$collected_order_invoices->addVehicleSubscriptionsTransaction();
|
||||
}
|
||||
|
||||
$collected_order_invoices->addToEconomic(true);
|
||||
}
|
||||
|
||||
(new logs_o())->add(
|
||||
'orderInvoices',
|
||||
'global',
|
||||
1,
|
||||
$user_id,
|
||||
'ADD_COLLECTED_INVOICE_ECONOMIC',
|
||||
'Queued transfer processed successfully for collected invoice #' . $collected_invoice_id
|
||||
);
|
||||
|
||||
return $collected_order_invoices->asArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function addTheDepartmentDateReference(economic_invoice_draft_mo $economic_invoice_draft, mixed $department_name, orders_o $order): void
|
||||
{
|
||||
$parsed_date = date('d/m/Y H:i', strtotime($order->created_at->value()));
|
||||
$economic_invoice_draft->addLineTEXT("[ " . $parsed_date . ' ' . $department_name . ' #' . $order->id . " ]");
|
||||
if ($order->reference->value() !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Reference:');
|
||||
if (str_contains($order->reference->value(), "\n")) {
|
||||
foreach (explode("\n", $order->reference->value()) as $line) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $order->reference->value());
|
||||
}
|
||||
}
|
||||
|
||||
$line_reg = '';
|
||||
if ($order->reg_1->value() !== '') {
|
||||
$line_reg .= 'Reg 1: ' . strtoupper($order->reg_1->value());
|
||||
}
|
||||
if ($order->reg_2->value() !== '') {
|
||||
$line_reg .= ', Reg 2: ' . strtoupper($order->reg_2->value());
|
||||
}
|
||||
if ($order->reg_3->value() !== '') {
|
||||
$line_reg .= ', Reg 3: ' . strtoupper($order->reg_3->value());
|
||||
}
|
||||
$economic_invoice_draft->addLineTEXT($line_reg);
|
||||
|
||||
if ($order->notes->value() !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Notat:');
|
||||
if (str_contains($order->notes->value(), "\n")) {
|
||||
foreach (explode("\n", $order->notes->value()) as $line) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $order->notes->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a product line was added, false when skipped.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function addOrderItemToInvoice(
|
||||
users_o $customer,
|
||||
orders_o $order,
|
||||
mixed $order_item,
|
||||
economic_invoice_draft_mo $economic_invoice_draft,
|
||||
int $quantity = 1
|
||||
): bool {
|
||||
if (self::shouldSkipOrderItemForInvoice($order_item, $quantity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_array($order_item)) {
|
||||
throw new Exception('Order item payload must be an array');
|
||||
}
|
||||
|
||||
$product_number = trim((string)($order_item['product']['economic_product_id'] ?? ''));
|
||||
if ($product_number === '') {
|
||||
throw new Exception('Order item is missing economic product id');
|
||||
}
|
||||
|
||||
$product_name = trim((string)($order_item['product']['name'] ?? ''));
|
||||
if ($product_name === '') {
|
||||
$product_name = 'Ukendt produkt';
|
||||
}
|
||||
|
||||
$reference = isset($order_item['reference']) ? (string)$order_item['reference'] : '';
|
||||
$notes = isset($order_item['notes']) ? (string)$order_item['notes'] : '';
|
||||
|
||||
$department = $order->getDepartmentByOrderId($order->id);
|
||||
$economic_department_id = $department['economic_department_id'];
|
||||
$economic_dimension_id = $department['economic_dimension_id'] ?? 0;
|
||||
$order_item_price = (float)($order_item['price'] ?? 0);
|
||||
$product_price = (float)($order_item['product']['price'] ?? 0);
|
||||
|
||||
$economic_invoice_draft->addLine(
|
||||
$product_number,
|
||||
$product_name,
|
||||
$quantity,
|
||||
$order_item_price,
|
||||
0,
|
||||
(int)$economic_department_id ?? 0,
|
||||
(int)$economic_dimension_id ?? 0
|
||||
);
|
||||
|
||||
$show_discount = abs($order_item_price - $product_price) > 0.00001;
|
||||
if ($show_discount && abs($product_price) > 0.00001) {
|
||||
$discount_percentage = round((($product_price - $order_item_price) / $product_price) * 100, 0);
|
||||
$economic_invoice_draft->addLineTEXT('Rabat: ' . ($order_item_price - $product_price) . ' DKK (' . $discount_percentage . '%)');
|
||||
}
|
||||
|
||||
if ($reference !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Reference:');
|
||||
if (str_contains($reference, "\n")) {
|
||||
foreach (explode("\n", $reference) as $line) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $reference);
|
||||
}
|
||||
}
|
||||
|
||||
if ($customer->doesUserHaveAttribute('requiresRegistrationNumbersInvoice')) {
|
||||
$line_reg = '';
|
||||
if ($order->reg_1->value() !== '') {
|
||||
$line_reg .= 'Reg 1: ' . strtoupper($order->reg_1->value());
|
||||
}
|
||||
if ($order->reg_2->value() !== '') {
|
||||
$line_reg .= ', Reg 2: ' . strtoupper($order->reg_2->value());
|
||||
}
|
||||
if ($order->reg_3->value() !== '') {
|
||||
$line_reg .= ', Reg 3: ' . strtoupper($order->reg_3->value());
|
||||
}
|
||||
$economic_invoice_draft->addLineTEXT($line_reg);
|
||||
}
|
||||
|
||||
if ($notes !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Notat:');
|
||||
if (str_contains($notes, "\n")) {
|
||||
foreach (explode("\n", $notes) as $line) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $notes);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function addOrderToInvoiceDraft(int $economic_invoice_draft_id, orders_o $order, users_o $customer, array $order_items): object
|
||||
{
|
||||
$has_billable_items = false;
|
||||
foreach ($order_items as $order_item) {
|
||||
if (!self::shouldSkipOrderItemForInvoice($order_item, (int)($order_item['quantity'] ?? 1))) {
|
||||
$has_billable_items = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$has_billable_items) {
|
||||
throw new Exception('No billable order items found');
|
||||
}
|
||||
|
||||
$economic_invoice_draft = new economic_invoice_draft_mo();
|
||||
$economic_invoice_draft->addLineTEXT('');
|
||||
$economic_invoice_draft->addLineTEXT('');
|
||||
$this->addTheDepartmentDateReference($economic_invoice_draft, $order->getDepartmentByOrderId($order->id)['name'], $order);
|
||||
|
||||
foreach ($order_items as $order_item) {
|
||||
$this->addOrderItemToInvoice($customer, $order, $order_item, $economic_invoice_draft, (int)($order_item['quantity'] ?? 1));
|
||||
}
|
||||
|
||||
return $economic_invoice_draft->addLinesToInvoiceDraft($economic_invoice_draft_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip item lines that are not billable in e-conomic export (0 quantity or 0 unit cost).
|
||||
*/
|
||||
public static function shouldSkipOrderItemForInvoice(mixed $order_item, int $quantity = 1): bool
|
||||
{
|
||||
if (!is_array($order_item)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$line_quantity = $quantity;
|
||||
if (isset($order_item['quantity']) && is_numeric($order_item['quantity'])) {
|
||||
$line_quantity = (int)$order_item['quantity'];
|
||||
}
|
||||
|
||||
if ($line_quantity <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$line_price = null;
|
||||
if (isset($order_item['price']) && is_numeric($order_item['price'])) {
|
||||
$line_price = (float)$order_item['price'];
|
||||
}
|
||||
|
||||
if ($line_price === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (abs($line_price) < 0.00001) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
use Exception;
|
||||
use mysqli_result;
|
||||
use objects\logs_o;
|
||||
|
||||
/**
|
||||
* Queue service for asynchronous e-conomic transfer jobs.
|
||||
*/
|
||||
class economic_transfer_queue
|
||||
{
|
||||
public const STATUS_QUEUED = 'QUEUED';
|
||||
public const STATUS_PROCESSING = 'PROCESSING';
|
||||
public const STATUS_COMPLETED = 'COMPLETED';
|
||||
public const STATUS_FAILED = 'FAILED';
|
||||
|
||||
public const TYPE_ORDER_DRAFT_EXPORT = 'ORDER_DRAFT_EXPORT';
|
||||
public const TYPE_ORDER_INVOICE_EXPORT = 'ORDER_INVOICE_EXPORT';
|
||||
public const TYPE_COLLECTED_INVOICE_EXPORT = 'COLLECTED_INVOICE_EXPORT';
|
||||
private const STALE_PROCESSING_LOCK_SECONDS = 900;
|
||||
|
||||
private economic_transfer_executor $executor;
|
||||
|
||||
public function __construct(?economic_transfer_executor $executor = null)
|
||||
{
|
||||
$this->executor = $executor ?? new economic_transfer_executor();
|
||||
economic_transfer_queue_schema_bootstrap::ensureTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function enqueue(string $transfer_type, array $payload, int $created_by = 0, int $max_attempts = 3): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$transfer_type = $this->validateTransferType($transfer_type);
|
||||
$max_attempts = max(1, min(10, $max_attempts));
|
||||
$created_by = max(0, $created_by);
|
||||
|
||||
$payload_json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($payload_json === false) {
|
||||
throw new Exception('Failed to serialize queue payload');
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO economic_transfer_queue_jobs
|
||||
(transfer_type, payload_json, status, progress_percent, progress_message, attempts, max_attempts, created_by)
|
||||
VALUES (?, ?, ?, 0, 'Queued', 0, ?, ?)"
|
||||
);
|
||||
if (!$stmt) {
|
||||
throw new Exception('Failed to prepare queue insert statement');
|
||||
}
|
||||
|
||||
$status = self::STATUS_QUEUED;
|
||||
$stmt->bind_param('sssii', $transfer_type, $payload_json, $status, $max_attempts, $created_by);
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception('Failed to enqueue transfer job');
|
||||
}
|
||||
$job_id = (int)$db->insert_id();
|
||||
$stmt->close();
|
||||
|
||||
(new logs_o())->add(
|
||||
'economic_transfer_queue',
|
||||
'global',
|
||||
1,
|
||||
$created_by,
|
||||
'ECONOMIC_TRANSFER_JOB_ENQUEUED',
|
||||
'Transfer job #' . $job_id . ' queued: ' . $transfer_type
|
||||
);
|
||||
|
||||
$job = $this->getJobById($job_id);
|
||||
if ($job === null) {
|
||||
throw new Exception('Failed to load queued transfer job');
|
||||
}
|
||||
return $job;
|
||||
}
|
||||
|
||||
public function getJobById(int $job_id): ?array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM economic_transfer_queue_jobs WHERE id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $job_id);
|
||||
if (!$stmt->execute()) {
|
||||
$stmt->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $stmt->get_result();
|
||||
$row = $result instanceof mysqli_result ? $result->fetch_assoc() : null;
|
||||
$stmt->close();
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
return $this->normalizeJobRow($row);
|
||||
}
|
||||
|
||||
public function listJobs(array $statuses = [], int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$limit = max(1, min(500, $limit));
|
||||
$offset = max(0, $offset);
|
||||
|
||||
$where = '';
|
||||
if (!empty($statuses)) {
|
||||
$clean_statuses = array_values(array_filter(array_map(static function ($status): string {
|
||||
return strtoupper(trim((string)$status));
|
||||
}, $statuses), static function ($status): bool {
|
||||
return in_array($status, [
|
||||
self::STATUS_QUEUED,
|
||||
self::STATUS_PROCESSING,
|
||||
self::STATUS_COMPLETED,
|
||||
self::STATUS_FAILED,
|
||||
], true);
|
||||
}));
|
||||
if (!empty($clean_statuses)) {
|
||||
$escaped = array_map(static function ($status) use ($db): string {
|
||||
return "'" . $db->escape_string($status) . "'";
|
||||
}, array_values(array_unique($clean_statuses)));
|
||||
$where = 'WHERE status IN (' . implode(',', $escaped) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM economic_transfer_queue_jobs $where ORDER BY id DESC LIMIT $limit OFFSET $offset";
|
||||
$result = $db->query($sql);
|
||||
if (!$result instanceof mysqli_result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$jobs = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$jobs[] = $this->normalizeJobRow($row);
|
||||
}
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function retryJob(int $job_id): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$existing_job = $this->getJobById($job_id);
|
||||
if ($existing_job === null) {
|
||||
throw new Exception('Queue job not found');
|
||||
}
|
||||
if ((string)($existing_job['status'] ?? '') !== self::STATUS_FAILED) {
|
||||
throw new Exception('Only failed jobs can be retried');
|
||||
}
|
||||
if ((int)($existing_job['attempts'] ?? 0) >= (int)($existing_job['max_attempts'] ?? 1)) {
|
||||
throw new Exception('Queue job reached max retry attempts');
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE economic_transfer_queue_jobs
|
||||
SET status = ?, progress_percent = 0, progress_message = 'Queued for retry',
|
||||
error_message = NULL, result_json = NULL, started_at = NULL, completed_at = NULL, locked_at = NULL
|
||||
WHERE id = ? AND status = ?"
|
||||
);
|
||||
if (!$stmt) {
|
||||
throw new Exception('Failed to prepare retry statement');
|
||||
}
|
||||
|
||||
$queued = self::STATUS_QUEUED;
|
||||
$failed = self::STATUS_FAILED;
|
||||
$stmt->bind_param('sis', $queued, $job_id, $failed);
|
||||
$stmt->execute();
|
||||
$affected = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
|
||||
if ($affected < 1) {
|
||||
throw new Exception('Failed to retry queue job');
|
||||
}
|
||||
|
||||
$job = $this->getJobById($job_id);
|
||||
if ($job === null) {
|
||||
throw new Exception('Retry updated job could not be loaded');
|
||||
}
|
||||
return $job;
|
||||
}
|
||||
|
||||
public function processPending(int $limit = 5): array
|
||||
{
|
||||
$limit = max(1, min(100, $limit));
|
||||
$this->releaseStaleProcessingLocks();
|
||||
|
||||
$processed = 0;
|
||||
$completed = 0;
|
||||
$failed = 0;
|
||||
$jobs = [];
|
||||
$empty_claims = 0;
|
||||
|
||||
for ($i = 0; $i < $limit; $i++) {
|
||||
$job = $this->claimNextJob();
|
||||
if ($job === null) {
|
||||
$empty_claims++;
|
||||
if ($empty_claims >= 3) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$empty_claims = 0;
|
||||
|
||||
$processed++;
|
||||
$jobs[] = $job['id'];
|
||||
$this->updateProgress((int)$job['id'], 15, 'Running transfer');
|
||||
|
||||
try {
|
||||
$result = $this->executeJob($job);
|
||||
$this->markCompleted((int)$job['id'], $result);
|
||||
$completed++;
|
||||
} catch (Exception $e) {
|
||||
$this->markFailed((int)$job['id'], $e->getMessage());
|
||||
$failed++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->markFailed((int)$job['id'], $e->getMessage());
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'processed' => $processed,
|
||||
'completed' => $completed,
|
||||
'failed' => $failed,
|
||||
'jobs' => $jobs,
|
||||
];
|
||||
}
|
||||
|
||||
private function claimNextJob(): ?array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$sql = "SELECT id FROM economic_transfer_queue_jobs
|
||||
WHERE status = '" . self::STATUS_QUEUED . "'
|
||||
AND attempts < max_attempts
|
||||
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
|
||||
ORDER BY id ASC
|
||||
LIMIT 1";
|
||||
$result = $db->query($sql);
|
||||
if (!$result instanceof mysqli_result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
if (!$row || !isset($row['id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$job_id = (int)$row['id'];
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE economic_transfer_queue_jobs
|
||||
SET status = ?, progress_percent = 5, progress_message = 'Processing', started_at = NOW(), locked_at = NOW()
|
||||
WHERE id = ? AND status = ?"
|
||||
);
|
||||
if (!$stmt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$processing = self::STATUS_PROCESSING;
|
||||
$queued = self::STATUS_QUEUED;
|
||||
$stmt->bind_param('sis', $processing, $job_id, $queued);
|
||||
$stmt->execute();
|
||||
$affected = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
|
||||
if ($affected < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getJobById($job_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function executeJob(array $job): array
|
||||
{
|
||||
$payload = (array)($job['payload'] ?? []);
|
||||
$transfer_type = (string)($job['transfer_type'] ?? '');
|
||||
$requested_by = (int)($payload['requested_by'] ?? ($job['created_by'] ?? 0));
|
||||
|
||||
$this->updateProgress((int)$job['id'], 40, 'Validating job payload');
|
||||
|
||||
return match ($transfer_type) {
|
||||
self::TYPE_ORDER_DRAFT_EXPORT => $this->executeOrderDraftExport($job, $payload, $requested_by),
|
||||
self::TYPE_ORDER_INVOICE_EXPORT => $this->executeOrderInvoiceExport($job, $payload, $requested_by),
|
||||
self::TYPE_COLLECTED_INVOICE_EXPORT => $this->executeCollectedInvoiceExport($job, $payload, $requested_by),
|
||||
default => throw new Exception('Unsupported transfer type: ' . $transfer_type),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function executeOrderDraftExport(array $job, array $payload, int $requested_by): array
|
||||
{
|
||||
$order_id = (int)($payload['order_id'] ?? 0);
|
||||
if ($order_id < 1) {
|
||||
throw new Exception('order_id is required');
|
||||
}
|
||||
$this->updateProgress((int)$job['id'], 65, 'Exporting order draft invoice');
|
||||
return $this->executor->exportOrderDraftInvoice($order_id, $requested_by);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function executeOrderInvoiceExport(array $job, array $payload, int $requested_by): array
|
||||
{
|
||||
$order_id = (int)($payload['order_id'] ?? 0);
|
||||
if ($order_id < 1) {
|
||||
throw new Exception('order_id is required');
|
||||
}
|
||||
$this->updateProgress((int)$job['id'], 65, 'Exporting booked invoice');
|
||||
return $this->executor->exportOrderInvoice($order_id, $requested_by);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function executeCollectedInvoiceExport(array $job, array $payload, int $requested_by): array
|
||||
{
|
||||
$collected_invoice_id = (int)($payload['collected_invoice_id'] ?? 0);
|
||||
if ($collected_invoice_id < 1) {
|
||||
throw new Exception('collected_invoice_id is required');
|
||||
}
|
||||
$send_as_is = (bool)($payload['send_as_is'] ?? false);
|
||||
$this->updateProgress((int)$job['id'], 65, 'Exporting collected invoice');
|
||||
return $this->executor->exportCollectedInvoice($collected_invoice_id, $send_as_is, $requested_by);
|
||||
}
|
||||
|
||||
private function updateProgress(int $job_id, int $percent, string $message): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
$percent = max(0, min(100, $percent));
|
||||
$escaped_message = $db->escape_string($message);
|
||||
$sql = "UPDATE economic_transfer_queue_jobs
|
||||
SET progress_percent = $percent, progress_message = '$escaped_message'
|
||||
WHERE id = $job_id";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
private function markCompleted(int $job_id, array $result): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
$result_json = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($result_json === false) {
|
||||
$result_json = json_encode(['result' => 'serialization_error']);
|
||||
}
|
||||
$escaped_result = $db->escape_string((string)$result_json);
|
||||
|
||||
$sql = "UPDATE economic_transfer_queue_jobs
|
||||
SET status = '" . self::STATUS_COMPLETED . "',
|
||||
progress_percent = 100,
|
||||
progress_message = 'Completed',
|
||||
result_json = '$escaped_result',
|
||||
error_message = NULL,
|
||||
completed_at = NOW(),
|
||||
locked_at = NULL
|
||||
WHERE id = $job_id";
|
||||
$db->query($sql);
|
||||
|
||||
(new logs_o())->add(
|
||||
'economic_transfer_queue',
|
||||
'global',
|
||||
1,
|
||||
0,
|
||||
'ECONOMIC_TRANSFER_JOB_COMPLETED',
|
||||
'Transfer job #' . $job_id . ' completed'
|
||||
);
|
||||
}
|
||||
|
||||
private function markFailed(int $job_id, string $error_message): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
$error_message = trim($error_message);
|
||||
if ($error_message === '') {
|
||||
$error_message = 'Unknown transfer queue error';
|
||||
}
|
||||
$escaped_error = $db->escape_string($error_message);
|
||||
|
||||
$sql = "UPDATE economic_transfer_queue_jobs
|
||||
SET status = '" . self::STATUS_FAILED . "',
|
||||
progress_message = 'Failed',
|
||||
error_message = '$escaped_error',
|
||||
attempts = attempts + 1,
|
||||
completed_at = NOW(),
|
||||
locked_at = NULL
|
||||
WHERE id = $job_id";
|
||||
$db->query($sql);
|
||||
|
||||
(new logs_o())->add(
|
||||
'economic_transfer_queue',
|
||||
'global',
|
||||
3,
|
||||
0,
|
||||
'ECONOMIC_TRANSFER_JOB_FAILED',
|
||||
'Transfer job #' . $job_id . ' failed: ' . $error_message
|
||||
);
|
||||
}
|
||||
|
||||
private function decodeJsonValue(mixed $json): mixed
|
||||
{
|
||||
if (!is_string($json) || trim($json) === '') {
|
||||
return null;
|
||||
}
|
||||
$decoded = json_decode($json, true);
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
|
||||
private function normalizeJobRow(array $row): array
|
||||
{
|
||||
return [
|
||||
'id' => (int)$row['id'],
|
||||
'transfer_type' => (string)($row['transfer_type'] ?? ''),
|
||||
'status' => (string)($row['status'] ?? self::STATUS_QUEUED),
|
||||
'progress_percent' => (int)($row['progress_percent'] ?? 0),
|
||||
'progress_message' => $row['progress_message'] ?? null,
|
||||
'attempts' => (int)($row['attempts'] ?? 0),
|
||||
'max_attempts' => (int)($row['max_attempts'] ?? 0),
|
||||
'error_message' => $row['error_message'] ?? null,
|
||||
'payload' => $this->decodeJsonValue($row['payload_json'] ?? null),
|
||||
'result' => $this->decodeJsonValue($row['result_json'] ?? null),
|
||||
'created_by' => isset($row['created_by']) ? (int)$row['created_by'] : null,
|
||||
'created_at' => $row['created_at'] ?? null,
|
||||
'updated_at' => $row['updated_at'] ?? null,
|
||||
'started_at' => $row['started_at'] ?? null,
|
||||
'completed_at' => $row['completed_at'] ?? null,
|
||||
'next_retry_at' => $row['next_retry_at'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Release jobs stuck in PROCESSING due to crashes or killed workers.
|
||||
*/
|
||||
private function releaseStaleProcessingLocks(): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
$timeout = (int)self::STALE_PROCESSING_LOCK_SECONDS;
|
||||
$sql = "UPDATE economic_transfer_queue_jobs
|
||||
SET status = '" . self::STATUS_QUEUED . "',
|
||||
progress_message = 'Re-queued after stale processing lock',
|
||||
locked_at = NULL,
|
||||
started_at = NULL
|
||||
WHERE status = '" . self::STATUS_PROCESSING . "'
|
||||
AND locked_at IS NOT NULL
|
||||
AND locked_at < (NOW() - INTERVAL $timeout SECOND)";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function validateTransferType(string $transfer_type): string
|
||||
{
|
||||
$transfer_type = strtoupper(trim($transfer_type));
|
||||
if (!in_array($transfer_type, [
|
||||
self::TYPE_ORDER_DRAFT_EXPORT,
|
||||
self::TYPE_ORDER_INVOICE_EXPORT,
|
||||
self::TYPE_COLLECTED_INVOICE_EXPORT,
|
||||
], true)) {
|
||||
throw new Exception('Unsupported transfer type: ' . $transfer_type);
|
||||
}
|
||||
return $transfer_type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
/**
|
||||
* Ensures additive schema for e-conomic transfer queue jobs.
|
||||
*/
|
||||
class economic_transfer_queue_schema_bootstrap
|
||||
{
|
||||
private static bool $initialized = false;
|
||||
|
||||
public static function ensureTables(): void
|
||||
{
|
||||
if (self::$initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $db;
|
||||
|
||||
$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS economic_transfer_queue_jobs (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
transfer_type VARCHAR(64) NOT NULL,
|
||||
payload_json JSON NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'QUEUED',
|
||||
progress_percent TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
progress_message VARCHAR(255) NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL DEFAULT 3,
|
||||
error_message TEXT NULL,
|
||||
result_json JSON NULL,
|
||||
created_by INT NULL,
|
||||
started_at DATETIME NULL,
|
||||
completed_at DATETIME NULL,
|
||||
next_retry_at DATETIME NULL,
|
||||
locked_at DATETIME NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_economic_transfer_queue_jobs_status_created (status, created_at),
|
||||
INDEX idx_economic_transfer_queue_jobs_next_retry (next_retry_at),
|
||||
INDEX idx_economic_transfer_queue_jobs_transfer_type (transfer_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ set_time_limit(10 * 60); // 10 minutes
|
||||
// Set the memory limit to 16 GB
|
||||
ini_set('memory_limit', '16G');
|
||||
|
||||
require_once __DIR__ . '/classes/economic_transfer_executor.php';
|
||||
require_once __DIR__ . '/classes/economic_transfer_queue_schema_bootstrap.php';
|
||||
require_once __DIR__ . '/classes/economic_transfer_queue.php';
|
||||
|
||||
|
||||
// If the first argument is 'run', switch to the second argument
|
||||
if ($args[1] === 'run') {
|
||||
@@ -88,6 +92,12 @@ if ($args[1] === 'run') {
|
||||
case 'logSync':
|
||||
require_once 'cron/SyncLogs.php';
|
||||
break;
|
||||
case 'economic-transfer-queue':
|
||||
echo "[" . date('Y-m-d H:i:s') . "][CRON] Running economic transfer queue worker\n";
|
||||
$queue = new \classes\economic_transfer_queue();
|
||||
$result = $queue->processPending(25);
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
break;
|
||||
case 'cron':
|
||||
echo "[" . date('Y-m-d H:i:s') . "][CRON] Running the cron script\n";
|
||||
require_once 'cron/Cron.php';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use classes\backup_store;
|
||||
use classes\economic;
|
||||
use classes\economic_transfer_queue;
|
||||
use classes\system_search_cache;
|
||||
use classes\system_search_document_index;
|
||||
use classes\system_search_economic_customer_index;
|
||||
@@ -25,6 +26,10 @@ use objects\logs_o;
|
||||
use objects\users_o;
|
||||
use routes\moduleWeatherAPIRoute;
|
||||
|
||||
require_once __DIR__ . '/../classes/economic_transfer_executor.php';
|
||||
require_once __DIR__ . '/../classes/economic_transfer_queue_schema_bootstrap.php';
|
||||
require_once __DIR__ . '/../classes/economic_transfer_queue.php';
|
||||
|
||||
if (!defined('WD')) {
|
||||
exit;
|
||||
}
|
||||
@@ -88,6 +93,12 @@ $cron_tasks = [
|
||||
'next_run' => 0,
|
||||
'function' => 'SyncEconomicInvoiceStatus',
|
||||
],
|
||||
'EconomicTransferQueueCron' => [
|
||||
'interval' => 30, // 30 seconds
|
||||
'last_run' => 0,
|
||||
'next_run' => 0,
|
||||
'function' => 'EconomicTransferQueueCron',
|
||||
],
|
||||
'SyncXLVaskModuleCron' => [
|
||||
'interval' => 3600, // 1 hour
|
||||
'last_run' => 0,
|
||||
@@ -229,6 +240,22 @@ function SyncXLVaskModuleCron(): void
|
||||
}
|
||||
}
|
||||
|
||||
function EconomicTransferQueueCron(): void
|
||||
{
|
||||
try {
|
||||
$queue = new economic_transfer_queue();
|
||||
$result = $queue->processPending(10);
|
||||
if ((int)($result['processed'] ?? 0) > 0) {
|
||||
echo "[" . date('Y-m-d H:i:s') . "][CRON] EconomicTransferQueueCron processed="
|
||||
. (int)($result['processed'] ?? 0)
|
||||
. " completed=" . (int)($result['completed'] ?? 0)
|
||||
. " failed=" . (int)($result['failed'] ?? 0) . "\n";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
warn('EconomicTransferQueueCron failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function SystemSearchCacheMaintenanceCron(): void
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -70,14 +70,28 @@ try {
|
||||
*/
|
||||
spl_autoload_register(function (string $class): void {
|
||||
$class = ltrim($class, '\\');
|
||||
$cache_key = 'autoload:' . $class;
|
||||
$is_loaded = static function (string $candidate): bool {
|
||||
return class_exists($candidate, false)
|
||||
|| interface_exists($candidate, false)
|
||||
|| trait_exists($candidate, false)
|
||||
|| (function_exists('enum_exists') && enum_exists($candidate, false));
|
||||
};
|
||||
|
||||
// Check Redis cache first
|
||||
if (defined('redis')) {
|
||||
try {
|
||||
$cached = redis->get('autoload:' . $class);
|
||||
if ($cached && is_file($cached)) {
|
||||
$cached = redis->get($cache_key);
|
||||
if (is_string($cached) && $cached !== '' && is_file($cached)) {
|
||||
require_once $cached;
|
||||
return;
|
||||
if ($is_loaded($class)) {
|
||||
return;
|
||||
}
|
||||
// Stale class mapping in cache, continue with normal lookup.
|
||||
redis->delete($cache_key);
|
||||
} elseif (is_string($cached) && $cached !== '') {
|
||||
// Remove non-existing cached path to avoid repeated failed lookups.
|
||||
redis->delete($cache_key);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fall back to manual search on Redis error
|
||||
@@ -137,10 +151,10 @@ spl_autoload_register(function (string $class): void {
|
||||
$file = $path . $suffix . '.php';
|
||||
if (is_file($file)) {
|
||||
require_once $file;
|
||||
if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false) || (function_exists('enum_exists') && enum_exists($class, false))) {
|
||||
if ($is_loaded($class)) {
|
||||
if (defined('redis')) {
|
||||
try {
|
||||
redis->setEx('autoload:' . $class, $file, 86400); // Cache for 24 hours
|
||||
redis->setEx($cache_key, $file, 86400); // Cache for 24 hours
|
||||
} catch (\Throwable $e) {}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -226,6 +226,9 @@ class economic_invoice_draft
|
||||
$total_discount = 0;
|
||||
// Loop through the order items
|
||||
foreach ( $order_items as $order_item ) {
|
||||
if ($this->shouldSkipOrderItemLine($order_item)) {
|
||||
continue;
|
||||
}
|
||||
// Add the order item to the draft invoice
|
||||
self::addOrderItemLine($order_item, $department);
|
||||
// Add the line discount to the total discount
|
||||
@@ -254,6 +257,9 @@ class economic_invoice_draft
|
||||
if (!isset($order_item['id'])) {
|
||||
throw new Exception('The order item is not valid');
|
||||
}
|
||||
if ($this->shouldSkipOrderItemLine($order_item)) {
|
||||
return;
|
||||
}
|
||||
// Get the department id
|
||||
$economic_department_id = $department['economic_department_id'] ?? 0;
|
||||
// Get the dimension id
|
||||
@@ -309,6 +315,25 @@ class economic_invoice_draft
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip line when quantity is zero/negative, or final unit price is zero.
|
||||
*/
|
||||
private function shouldSkipOrderItemLine(array $order_item): bool
|
||||
{
|
||||
$quantity = isset($order_item['quantity']) && is_numeric($order_item['quantity'])
|
||||
? (float)$order_item['quantity']
|
||||
: 0.0;
|
||||
if ($quantity <= 0.0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$price = isset($order_item['price']) && is_numeric($order_item['price'])
|
||||
? (float)$order_item['price']
|
||||
: 0.0;
|
||||
|
||||
return abs($price) < 0.00001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a product line to the draft invoice
|
||||
* @note The lines won't be saved until the addLines() method is called.
|
||||
@@ -428,4 +453,4 @@ class economic_invoice_draft
|
||||
throw new Exception('The draft invoice data is not set');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1482,4 +1482,9 @@ class collected_order_invoices_o extends db
|
||||
$row = $result->fetch_assoc();
|
||||
return (int)$row['count'] === 0;
|
||||
}
|
||||
|
||||
public function clearCachedData(): void
|
||||
{
|
||||
$this->objectChanged();
|
||||
}
|
||||
}
|
||||
@@ -1676,4 +1676,45 @@ class orders_o extends db
|
||||
}
|
||||
return $orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getOrdersWithRegistrationNumberInDateRange(string $registration_number, string $from_date, string $to_date): array
|
||||
{
|
||||
global /** @var db $db */
|
||||
$db;
|
||||
// Validate the date range
|
||||
$from_timestamp = strtotime($from_date);
|
||||
$to_timestamp = strtotime($to_date);
|
||||
if ($from_timestamp === false || $to_timestamp === false) {
|
||||
throw new Exception('Invalid date range provided');
|
||||
}
|
||||
if ($from_timestamp > $to_timestamp) {
|
||||
throw new Exception('The start date cannot be after the end date');
|
||||
}
|
||||
// Prepare the SQL query to find orders with the registration number in the date range
|
||||
$reg = strtoupper(trim($registration_number));
|
||||
if ($reg === '') {
|
||||
return [];
|
||||
}
|
||||
$reg = $db->escape_string($reg);
|
||||
$from_date = $db->escape_string(date('Y-m-d H:i:s', $from_timestamp));
|
||||
$to_date = $db->escape_string(date('Y-m-d H:i:s', $to_timestamp));
|
||||
$sql = "SELECT id FROM $this->table
|
||||
WHERE (UPPER(TRIM(reg_1)) = '$reg' OR UPPER(TRIM(reg_2)) = '$reg' OR UPPER(TRIM(reg_3)) = '$reg')
|
||||
AND created_at BETWEEN '$from_date' AND '$to_date'
|
||||
AND deleted_at IS NULL";
|
||||
$result = $db->query($sql);
|
||||
if ($result->num_rows === 0) {
|
||||
return []; // No orders found with the registration number in the date range
|
||||
}
|
||||
$orders = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$order = new orders_o();
|
||||
$order->select((int)$row['id']);
|
||||
$orders[] = $order;
|
||||
}
|
||||
return $orders;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,14 +3,13 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use classes\economic_transfer_queue;
|
||||
use classes\response;
|
||||
use classes\router;
|
||||
use economic_invoice_draft_mo;
|
||||
use objects\departments_o;
|
||||
use objects\economic_module_orders;
|
||||
use objects\logs_o;
|
||||
use objects\orders_o;
|
||||
use objects\users_o;
|
||||
use traits\route_t;
|
||||
|
||||
class economicInvoiceRoute
|
||||
@@ -23,118 +22,54 @@ class economicInvoiceRoute
|
||||
/** @var router $router */
|
||||
$router, $response;
|
||||
|
||||
|
||||
$this->post('/economic/invoice/draft/export', function () {
|
||||
global $response;
|
||||
$this->requirePermission('economic_invoice_draft_export');
|
||||
$user = (new authentication())->get_user();
|
||||
if ($user) {
|
||||
$order_id = $response->getRequestParameter('order_id');
|
||||
if (!isset($order_id)) {
|
||||
$response->error('Order ID is required', 400);
|
||||
}
|
||||
// Validate the order ID is a number
|
||||
if (!is_numeric($order_id)) {
|
||||
$response->error('Order ID must be a number', 400);
|
||||
}
|
||||
$order = (new orders_o())->getOrderById($order_id);
|
||||
// Check if the order exists
|
||||
if (!$order->exists()) {
|
||||
$response->error('Order not found', 404);
|
||||
}
|
||||
// Get the order items
|
||||
$order_items = (new orders_o())->getOrderItems($order_id);
|
||||
// Apply the department pricing
|
||||
$order_items = (new orders_o())->applyDepartmentPrices($order_items, $order->department_id->value());
|
||||
// Get the customer
|
||||
$customer = (new orders_o())->getCustomerByOrderId($order_id);
|
||||
// Check if the customer exists
|
||||
if (!$customer->exists()) {
|
||||
$response->error('Customer not found', 404);
|
||||
}
|
||||
// Get the customer economic number
|
||||
$customer_economic = $customer->getCustomerEcocomicData()->economic_customer;
|
||||
$economic_invoice_draft = (new economic_invoice_draft_mo());
|
||||
// Set the customer number
|
||||
$economic_invoice_draft->setCustomerNumber((int)$customer_economic->customer_number);
|
||||
// Set the recipient
|
||||
$economic_invoice_draft->setRecipient(
|
||||
$customer_economic->name ?? 'Ukendt',
|
||||
$customer_economic->address ?? 'Ukendt',
|
||||
$customer_economic->zip ?? 'Ukendt',
|
||||
$customer_economic->city ?? 'Ukendt'
|
||||
);
|
||||
// Make sure there are order items
|
||||
if (count($order_items) === 0) {
|
||||
$response->error('No order items found', 404);
|
||||
}
|
||||
// Get the department
|
||||
$department = (new departments_o())->getDepartmentById($order->department_id->value());
|
||||
// Add the department, date, reference
|
||||
$this->addTheDepartmentDateReference($economic_invoice_draft, $department['name'], $order);
|
||||
|
||||
// Add the lines to the invoice
|
||||
foreach ( $order_items as $order_item ) {
|
||||
// Add the order item to the invoice draft
|
||||
$this->addOrderItemToInvoice(
|
||||
$customer,
|
||||
$order,
|
||||
$order_item,
|
||||
$economic_invoice_draft,
|
||||
$order_item['quantity'] ?? 1);
|
||||
}
|
||||
// Check if the user has an open invoice draft
|
||||
$hasOpenInvoiceDraft = $customer->hasOpenInvoiceDraft();
|
||||
if ($hasOpenInvoiceDraft && !$customer->invoicePerOrder()) {
|
||||
// Get the open invoice draft
|
||||
$openInvoiceDraft = $customer->getOpenInvoiceDraft();
|
||||
// Add the order to the invoice draft
|
||||
$result = $this->addOrderToInvoiceDraft($openInvoiceDraft, $order, $customer, $order_items);
|
||||
}
|
||||
// If the customer doesn't want to be billed per order, or if there's no open invoice draft, we'll create a new one
|
||||
// Create the invoice draft
|
||||
if (!isset($result)) {
|
||||
$result = $economic_invoice_draft->createInvoiceDraftExample();
|
||||
}
|
||||
// Check if the invoice draft was created, or if the lines were added (lines is an array, and should not be empty)
|
||||
if (!isset($result->draftInvoiceNumber) && !isset($result->lines[0])) {
|
||||
// Log the error
|
||||
(new logs_o())->add('economic_invoice_draft', 'global', 3, $user->id, 'ECONOMIC_INVOICE_DRAFT_EXPORT', 'Failed to create economic invoice draft');
|
||||
// Check if we can get the errors from the response
|
||||
if (isset($result->errors)) {
|
||||
$response->add_meta('economic_errors', $result->errors);
|
||||
}
|
||||
$response->add_meta('economic_result', $result);
|
||||
// Try to parse the error message
|
||||
$response->error($result->message ?? 'Failed to create economic invoice draft', 500);
|
||||
}
|
||||
// Add the economic invoice draft to the order
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
// Remove the existing invoice draft (if any)
|
||||
if ($economic_module_orders->economic_invoice_draft_id->value() > 0) {
|
||||
$economic_invoice_draft->deleteInvoiceDraft($economic_module_orders->economic_invoice_draft_id->value());
|
||||
}
|
||||
$economic_module_orders->economic_invoice_draft_id->set($result->draftInvoiceNumber ?? $customer->getOpenInvoiceDraft());
|
||||
if (!$customer->invoicePerOrder()) {
|
||||
// If the customer wants to be billed per order, we'll add the order to the invoice draft
|
||||
$customer->setOpenInvoiceDraft($result->draftInvoiceNumber ?? $customer->getOpenInvoiceDraft());
|
||||
} else {
|
||||
// If the customer doesn't want to be billed per order, we'll remove the open invoice draft
|
||||
$customer->unsetOpenInvoiceDraft();
|
||||
}
|
||||
// Return the response
|
||||
(new logs_o())->add('economic_invoice_draft', 'global', 1, $user->id, 'ECONOMIC_INVOICE_DRAFT_EXPORT', 'Successfully exported an economic invoice draft');
|
||||
$response->success($economic_module_orders->getArray());
|
||||
} else {
|
||||
if (!$user) {
|
||||
(new logs_o())->add('economic_invoice_draft', 'global', 1, 0, 'ECONOMIC_INVOICE_DRAFT_EXPORT', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$order_id = $response->getRequestParameter('order_id');
|
||||
if (!isset($order_id)) {
|
||||
$response->error('Order ID is required', 400);
|
||||
}
|
||||
if (!is_numeric($order_id) || (int)$order_id < 1) {
|
||||
$response->error('Order ID must be a positive number', 400);
|
||||
}
|
||||
|
||||
$order = (new orders_o())->getOrderById((int)$order_id);
|
||||
if (!$order->exists()) {
|
||||
$response->error('Order not found', 404);
|
||||
}
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->enqueue(
|
||||
economic_transfer_queue::TYPE_ORDER_DRAFT_EXPORT,
|
||||
[
|
||||
'order_id' => (int)$order_id,
|
||||
'requested_by' => (int)$user->id,
|
||||
],
|
||||
(int)$user->id
|
||||
);
|
||||
$job_id = (int)($job['id'] ?? 0);
|
||||
if ($job_id < 1) {
|
||||
$response->error('Failed to enqueue draft export job: missing queue job id in enqueue response', 500);
|
||||
}
|
||||
|
||||
(new logs_o())->add('economic_invoice_draft', 'global', 1, $user->id, 'ECONOMIC_INVOICE_DRAFT_EXPORT_QUEUED', 'Queued economic invoice draft export');
|
||||
$response->success([
|
||||
'message' => 'Economic invoice draft export queued',
|
||||
'job_id' => $job_id,
|
||||
'job' => $job,
|
||||
], 202);
|
||||
}, [
|
||||
'economic_invoice_draft_export' => 'Export an economic invoice draft'
|
||||
]);
|
||||
|
||||
$this->delete('/economic/invoice/draft/delete', function () {
|
||||
|
||||
global /** @var response $response */
|
||||
$response;
|
||||
$this->requirePermission('economic_invoice_draft_delete');
|
||||
@@ -145,34 +80,25 @@ class economicInvoiceRoute
|
||||
$response->error('Order ID is required', 400);
|
||||
}
|
||||
$order = (new orders_o())->getOrderById($order_id);
|
||||
// Check if the order exists
|
||||
if (!$order->exists()) {
|
||||
$response->error('Order not found', 404);
|
||||
}
|
||||
// Make sure the order has an economic invoice draft
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
if ($economic_module_orders->economic_invoice_draft_id->value() === 0) {
|
||||
$response->error('No economic invoice draft found', 404);
|
||||
}
|
||||
$economic_invoice_draft = (new economic_invoice_draft_mo());
|
||||
// Get the invoice draft number
|
||||
$invoiceDraftId = $economic_module_orders->economic_invoice_draft_id->value();
|
||||
$customer = (new orders_o())->getCustomerByOrderId($order_id);
|
||||
if (!$customer->invoicePerOrder()) {
|
||||
// Check if the customer has an open invoice draft
|
||||
if ($customer->hasOpenInvoiceDraft()) {
|
||||
// Check if it's the same as the order's invoice draft
|
||||
if ((int)$customer->getOpenInvoiceDraft() === (int)$invoiceDraftId) {
|
||||
// Remove the open invoice draft from the customer
|
||||
$customer->deleteOpenInvoiceDraft();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete the invoice draft
|
||||
$economic_invoice_draft->deleteInvoiceDraft($invoiceDraftId);
|
||||
// Remove the economic invoice draft from the order
|
||||
$economic_module_orders->economic_invoice_draft_id->set(null);
|
||||
// Return the response
|
||||
(new logs_o())->add('economic_invoice_draft', 'global', 1, $user->id, 'ECONOMIC_INVOICE_DRAFT_DELETE', 'Successfully deleted an economic invoice draft');
|
||||
$response->success($economic_module_orders->getArray());
|
||||
} else {
|
||||
@@ -187,216 +113,189 @@ class economicInvoiceRoute
|
||||
global $response;
|
||||
$this->requirePermission('economic_invoice_export');
|
||||
$user = (new authentication())->get_user();
|
||||
if ($user) {
|
||||
$order_id = $response->getRequestParameter('order_id');
|
||||
if (!isset($order_id)) {
|
||||
$response->error('Order ID is required', 400);
|
||||
}
|
||||
$order = (new orders_o())->getOrderById($order_id);
|
||||
// Check if the order exists
|
||||
if (!$order->exists()) {
|
||||
$response->error('Order not found', 404);
|
||||
}
|
||||
// Get the customer
|
||||
$customer = (new orders_o())->getCustomerByOrderId($order_id);
|
||||
// Check if the customer exists
|
||||
if (!$customer->exists()) {
|
||||
$response->error('Customer not found', 404);
|
||||
}
|
||||
// Make sure the order has an economic invoice draft
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
if ($economic_module_orders->economic_invoice_draft_id->value() === 0) {
|
||||
$response->error('No economic invoice draft found', 404);
|
||||
}
|
||||
$economic_invoice_draft = (new economic_invoice_draft_mo());
|
||||
// Get the invoice draft number
|
||||
$invoiceDraftId = $economic_module_orders->economic_invoice_draft_id->value();
|
||||
// Make sure there's not already an invoice created
|
||||
$invoiceId = $economic_module_orders->economic_invoice_id->value();
|
||||
if ($invoiceId > 0) {
|
||||
$response->error('An invoice has already been created, invoice ID: ' . $invoiceId, 400);
|
||||
}
|
||||
// Publish the invoice draft
|
||||
$result = $economic_invoice_draft->publishInvoiceDraft((int)$invoiceDraftId);
|
||||
// Check if the invoice was created
|
||||
if (!isset($result->bookedInvoiceNumber)) {
|
||||
// Log the error
|
||||
(new logs_o())->add('economic_invoice', 'global', 3, $user->id, 'ECONOMIC_INVOICE_EXPORT', 'Failed to create economic invoice from draft: ' . $invoiceDraftId);
|
||||
// Check if we can get the errors from the response
|
||||
if (isset($result->errors)) {
|
||||
$response->add_meta('economic_errors', $result->errors);
|
||||
}
|
||||
// Try to parse the error message
|
||||
$response->error($result->message ?? 'Failed to create economic invoice', 500);
|
||||
}
|
||||
// Add the economic invoice to the order
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId($order_id);
|
||||
$economic_module_orders->economic_invoice_id->set($result->bookedInvoiceNumber);
|
||||
// Remove the economic invoice draft from the customer
|
||||
$customer->unsetOpenInvoiceDraft();
|
||||
// Return the response
|
||||
(new logs_o())->add('economic_invoice', 'global', 1, $user->id, 'ECONOMIC_INVOICE_EXPORT', 'Successfully exported an economic invoice');
|
||||
$response->success($economic_module_orders->getArray());
|
||||
} else {
|
||||
if (!$user) {
|
||||
(new logs_o())->add('economic_invoice', 'global', 1, 0, 'ECONOMIC_INVOICE_EXPORT', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$order_id = $response->getRequestParameter('order_id');
|
||||
if (!isset($order_id)) {
|
||||
$response->error('Order ID is required', 400);
|
||||
}
|
||||
if (!is_numeric($order_id) || (int)$order_id < 1) {
|
||||
$response->error('Order ID must be a positive number', 400);
|
||||
}
|
||||
|
||||
$order = (new orders_o())->getOrderById((int)$order_id);
|
||||
if (!$order->exists()) {
|
||||
$response->error('Order not found', 404);
|
||||
}
|
||||
$customer = (new orders_o())->getCustomerByOrderId((int)$order_id);
|
||||
if (!$customer->exists()) {
|
||||
$response->error('Customer not found', 404);
|
||||
}
|
||||
$economic_module_orders = (new economic_module_orders())->getByOrderId((int)$order_id);
|
||||
if ($economic_module_orders->economic_invoice_draft_id->value() === 0) {
|
||||
$response->error('No economic invoice draft found', 404);
|
||||
}
|
||||
if ((int)$economic_module_orders->economic_invoice_id->value() > 0) {
|
||||
$response->error('An invoice has already been created, invoice ID: ' . (int)$economic_module_orders->economic_invoice_id->value(), 400);
|
||||
}
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->enqueue(
|
||||
economic_transfer_queue::TYPE_ORDER_INVOICE_EXPORT,
|
||||
[
|
||||
'order_id' => (int)$order_id,
|
||||
'requested_by' => (int)$user->id,
|
||||
],
|
||||
(int)$user->id
|
||||
);
|
||||
$job_id = (int)($job['id'] ?? 0);
|
||||
if ($job_id < 1) {
|
||||
$response->error('Failed to enqueue invoice export job: missing queue job id in enqueue response', 500);
|
||||
}
|
||||
|
||||
(new logs_o())->add('economic_invoice', 'global', 1, $user->id, 'ECONOMIC_INVOICE_EXPORT_QUEUED', 'Queued economic invoice export');
|
||||
$response->success([
|
||||
'message' => 'Economic invoice export queued',
|
||||
'job_id' => $job_id,
|
||||
'job' => $job,
|
||||
], 202);
|
||||
}, [
|
||||
'economic_invoice_export' => 'Export an economic invoice'
|
||||
]);
|
||||
|
||||
$this->get('/economic/invoice/draft/export/status', function () {
|
||||
global $response;
|
||||
$this->requirePermission('economic_invoice_draft_export');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$job_id = $response->getRequestParameter('job_id');
|
||||
if (!isset($job_id) || !is_numeric($job_id) || (int)$job_id < 1) {
|
||||
$response->error('job_id is required and must be a positive number', 400);
|
||||
}
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->getJobById((int)$job_id);
|
||||
if ($job === null || ($job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_ORDER_DRAFT_EXPORT) {
|
||||
$response->error('Draft export queue job not found', 404);
|
||||
}
|
||||
|
||||
$response->success($job);
|
||||
}, [
|
||||
'economic_invoice_draft_export' => 'Read queue status for economic draft invoice export'
|
||||
]);
|
||||
|
||||
$this->post('/economic/invoice/draft/export/retry', function () {
|
||||
global $response;
|
||||
$this->requirePermission('economic_invoice_draft_export');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$job_id = $response->getRequestParameter('job_id');
|
||||
if (!isset($job_id) || !is_numeric($job_id) || (int)$job_id < 1) {
|
||||
$response->error('job_id is required and must be a positive number', 400);
|
||||
}
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$existing_job = $queue->getJobById((int)$job_id);
|
||||
if ($existing_job === null || ($existing_job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_ORDER_DRAFT_EXPORT) {
|
||||
$response->error('Draft export queue job not found', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$job = $queue->retryJob((int)$job_id);
|
||||
} catch (\Throwable $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$response->success([
|
||||
'message' => 'Draft export queue job retried',
|
||||
'job' => $job,
|
||||
]);
|
||||
}, [
|
||||
'economic_invoice_draft_export' => 'Retry failed queue job for economic draft invoice export'
|
||||
]);
|
||||
|
||||
$this->get('/economic/invoice/export/status', function () {
|
||||
global $response;
|
||||
$this->requirePermission('economic_invoice_export');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$job_id = $response->getRequestParameter('job_id');
|
||||
if (!isset($job_id) || !is_numeric($job_id) || (int)$job_id < 1) {
|
||||
$response->error('job_id is required and must be a positive number', 400);
|
||||
}
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->getJobById((int)$job_id);
|
||||
if ($job === null || ($job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_ORDER_INVOICE_EXPORT) {
|
||||
$response->error('Invoice export queue job not found', 404);
|
||||
}
|
||||
|
||||
$response->success($job);
|
||||
}, [
|
||||
'economic_invoice_export' => 'Read queue status for economic invoice export'
|
||||
]);
|
||||
|
||||
$this->post('/economic/invoice/export/retry', function () {
|
||||
global $response;
|
||||
$this->requirePermission('economic_invoice_export');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$job_id = $response->getRequestParameter('job_id');
|
||||
if (!isset($job_id) || !is_numeric($job_id) || (int)$job_id < 1) {
|
||||
$response->error('job_id is required and must be a positive number', 400);
|
||||
}
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$existing_job = $queue->getJobById((int)$job_id);
|
||||
if ($existing_job === null || ($existing_job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_ORDER_INVOICE_EXPORT) {
|
||||
$response->error('Invoice export queue job not found', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$job = $queue->retryJob((int)$job_id);
|
||||
} catch (\Throwable $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$response->success([
|
||||
'message' => 'Invoice export queue job retried',
|
||||
'job' => $job,
|
||||
]);
|
||||
}, [
|
||||
'economic_invoice_export' => 'Retry failed queue job for economic invoice export'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param economic_invoice_draft_mo $economic_invoice_draft
|
||||
* @param $department_name
|
||||
* @param orders_o $order
|
||||
* @return void
|
||||
*/
|
||||
function addTheDepartmentDateReference(economic_invoice_draft_mo $economic_invoice_draft, $department_name, orders_o $order): void
|
||||
private function ensureEconomicTransferQueueIsAvailable(): void
|
||||
{
|
||||
// The format is:
|
||||
// Truck Wash - [department name], [date], (?)Ref(erence): [reference], Reg 1: [reg_1], (?)Reg 2: [reg_2], (?)Reg 3: [reg_3]
|
||||
// (?)[note]
|
||||
// Example:
|
||||
// 12-02-2025 13:27, Truck Wash - Administration, Ref: 123456, Reg 1: ABC123, Reg 2: DEF456, Reg 3: GHI789
|
||||
// This is a note for the invoice
|
||||
//
|
||||
// (?) = Optional
|
||||
$parsed_date = date('d/m/Y H:i', strtotime($order->created_at->value()));
|
||||
$economic_invoice_draft->addLineTEXT("[ " . $parsed_date . ' ' . $department_name . ' #' . $order->id . " ]");
|
||||
// If there's a reference, add it to the invoice
|
||||
if ($order->reference->value() !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Reference:');
|
||||
// Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once)
|
||||
if (str_contains($order->reference->value(), "\n")) {
|
||||
foreach ( explode("\n", $order->reference->value()) as $line ) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $order->reference->value());
|
||||
}
|
||||
}
|
||||
// Add the registration numbers (if any)
|
||||
$line_reg = '';
|
||||
if ($order->reg_1->value() !== '') {
|
||||
$line_reg .= 'Reg 1: ' . strtoupper($order->reg_1->value());
|
||||
}
|
||||
if ($order->reg_2->value() !== '')
|
||||
$line_reg .= ', Reg 2: ' . strtoupper($order->reg_2->value());
|
||||
if ($order->reg_3->value() !== '')
|
||||
$line_reg .= ', Reg 3: ' . strtoupper($order->reg_3->value());
|
||||
// Add the line to the invoice
|
||||
$economic_invoice_draft->addLineTEXT($line_reg);
|
||||
// If there's a note, add it to the invoice
|
||||
if ($order->notes->value() !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Notat:');
|
||||
// Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once)
|
||||
if (str_contains($order->notes->value(), "\n")) {
|
||||
foreach ( explode("\n", $order->notes->value()) as $line ) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $order->notes->value());
|
||||
}
|
||||
global $response;
|
||||
|
||||
if (
|
||||
!class_exists('\\classes\\economic_transfer_executor')
|
||||
|| !class_exists('\\classes\\economic_transfer_queue_schema_bootstrap')
|
||||
|| !class_exists(economic_transfer_queue::class)
|
||||
) {
|
||||
$response->error('Economic transfer queue is unavailable in this deployment', 503);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param users_o $customer
|
||||
* @param orders_o $order
|
||||
* @param mixed $order_item
|
||||
* @param economic_invoice_draft_mo $economic_invoice_draft
|
||||
* @param int $quantity
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
function addOrderItemToInvoice(users_o $customer, orders_o $order, mixed $order_item, economic_invoice_draft_mo $economic_invoice_draft, int $quantity = 1): void
|
||||
{
|
||||
// The format is:
|
||||
// [product name], (?)Ref(erence): [reference], (!?)Reg 1: [reg 1], (!?)Reg 2: [reg 2], (!?)Reg 3: [reg 3], (?)Note: [note], (?)Discount: ([order item price] - [product price]) DKK ([discount percentage] %)
|
||||
// (?) = Optional
|
||||
// (!) = Required if the customer requires it
|
||||
// (!?) = Should be added if the customer requires it
|
||||
// Example:
|
||||
// Tankcleaning 4 spulehoveder, Ref: 123456, Reg 1: ABC123, Reg 2: DEF456, Reg 3: GHI789, Note: This is a note, Discount: -100 DKK (20%)
|
||||
// Get the department
|
||||
$department = $order->getDepartmentByOrderId($order->id);
|
||||
$economic_department_id = $department['economic_department_id'];
|
||||
$economic_dimension_id = $department['economic_dimension_id'] ?? 0;
|
||||
|
||||
// Add the line to the invoice
|
||||
$economic_invoice_draft->addLine(
|
||||
(string)$order_item['product']['economic_product_id'],
|
||||
(string)$order_item['product']['name'],
|
||||
(int)$quantity,
|
||||
(int)$order_item['price'],
|
||||
0, // Since we can't be specific about the discount, we set it to 0. (The API has a limit of 2 decimals, and that's not enough for our needs)
|
||||
(int)$economic_department_id ?? 0,
|
||||
(int)$economic_dimension_id ?? 0 // If the department is not set, we'll set it to 0
|
||||
);
|
||||
// Calculate the discount percentage
|
||||
$discountPercentage = round((($order_item['product']['price'] - $order_item['price']) / $order_item['product']['price']) * 100, 0);
|
||||
|
||||
// If the price is different from the product price, add it to the line
|
||||
if ($order_item['price'] !== $order_item['product']['price'])
|
||||
$economic_invoice_draft->addLineTEXT('Rabat: ' . ($order_item['price'] - $order_item['product']['price']) . ' DKK (' . $discountPercentage . '%)');
|
||||
|
||||
// If there's a reference, add it to the line
|
||||
if ($order_item['reference'] !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Reference:');
|
||||
// Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once)
|
||||
if (str_contains($order_item['reference'], "\n")) {
|
||||
foreach ( explode("\n", $order_item['reference']) as $line ) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $order_item['reference']);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the registration numbers (if they exist, and the customer requires it)
|
||||
if ($customer->doesUserHaveAttribute('requiresRegistrationNumbersInvoice')) {
|
||||
$line_reg = '';
|
||||
if ($order->reg_1->value() !== '') {
|
||||
$line_reg .= 'Reg 1: ' . strtoupper($order->reg_1->value());
|
||||
}
|
||||
if ($order->reg_2->value() !== '') {
|
||||
$line_reg .= ', Reg 2: ' . strtoupper($order->reg_2->value());
|
||||
}
|
||||
if ($order->reg_3->value() !== '') {
|
||||
$line_reg .= ', Reg 3: ' . strtoupper($order->reg_3->value());
|
||||
}
|
||||
$economic_invoice_draft->addLineTEXT($line_reg);
|
||||
}
|
||||
|
||||
// If there's a note, add it to the line
|
||||
if ($order_item['notes'] !== '') {
|
||||
$economic_invoice_draft->addLineTEXT('Notat:');
|
||||
// Add "# " in the beginning of each line (If there's more than one line, otherwise we just add the prefix once)
|
||||
if (str_contains($order_item['notes'], "\n")) {
|
||||
foreach ( explode("\n", $order_item['notes']) as $line ) {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $line);
|
||||
}
|
||||
} else {
|
||||
$economic_invoice_draft->addLineTEXT('# ' . $order_item['notes']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function addOrderToInvoiceDraft(int $economic_invoice_draft_id, orders_o $order, users_o $customer, array $order_items): object
|
||||
{
|
||||
$economic_invoice_draft = (new economic_invoice_draft_mo());
|
||||
// Add two empty lines to the invoice, to separate the orders
|
||||
$economic_invoice_draft->addLineTEXT('');
|
||||
$economic_invoice_draft->addLineTEXT('');
|
||||
// Add the department, date, reference
|
||||
$this->addTheDepartmentDateReference($economic_invoice_draft, $order->getDepartmentByOrderId($order->id)['name'], $order);
|
||||
// Add the lines to the invoice
|
||||
foreach ( $order_items as $order_item ) {
|
||||
// Add the order item to the invoice draft
|
||||
$this->addOrderItemToInvoice($customer, $order, $order_item, $economic_invoice_draft, $order_item['quantity'] ?? 1);
|
||||
}
|
||||
return $economic_invoice_draft->addLinesToInvoiceDraft($economic_invoice_draft_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use classes\economic;
|
||||
use classes\economic_transfer_queue;
|
||||
use classes\economic_v2_compare_engine;
|
||||
use classes\economic_v2_line_normalizer;
|
||||
use classes\economic_v2_revenue_statistics_service;
|
||||
@@ -13,6 +14,7 @@ use Exception;
|
||||
use objects\collected_order_invoices_o;
|
||||
use objects\customer_fixed_pricing_o;
|
||||
use objects\logs_o;
|
||||
use objects\orders_o;
|
||||
use objects\users_o;
|
||||
use traits\route_t;
|
||||
|
||||
@@ -522,77 +524,75 @@ class orderInvoicesRoute
|
||||
]
|
||||
);
|
||||
|
||||
/** Collected order invoices > E-Conomic > POST */
|
||||
/** Collected order invoices > E-Conomic > POST (queued) */
|
||||
$this->post('/collected-invoices/economic', function () {
|
||||
global $response;
|
||||
self::requirePermission('add_collected_invoice_economic');
|
||||
$user = (new authentication())->get_user();
|
||||
if ($user) {
|
||||
(new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_ECONOMIC', 'User added a collected order invoice to E-Conomic');
|
||||
// Require the ID, and validate its type and length
|
||||
self::requireParameters(['id']);
|
||||
self::requireType((int)self::getParameter('id'), self::type_int());
|
||||
self::requireMinLength('id', 1);
|
||||
self::requireMaxLength('id', 10);
|
||||
// Require the ID to be above 0
|
||||
self::requireMinValue((int)self::getParameter('id'), 1);
|
||||
// If the send_as_is parameter is set, validate its type
|
||||
$send_as_is = false; // This is false by default, when true it will send the invoice as is, without adding vehicle subscriptions or fixed pricing. This is used when a customer with fixed pricing has already been invoiced with the fixed price, and we just need to send the invoice to E-Conomic.
|
||||
|
||||
$send_as_is = false;
|
||||
if (self::isParametersSet(['send_as_is'])) {
|
||||
self::requireType((bool)self::getParameter('send_as_is'), self::type_bool());
|
||||
$send_as_is = (bool)self::getParameter('send_as_is');
|
||||
$send_as_is_raw = self::getParameter('send_as_is');
|
||||
if (is_bool($send_as_is_raw)) {
|
||||
$send_as_is = $send_as_is_raw;
|
||||
} elseif (is_numeric($send_as_is_raw)) {
|
||||
$send_as_is = ((int)$send_as_is_raw) === 1;
|
||||
} elseif (is_string($send_as_is_raw)) {
|
||||
$normalized_send_as_is = strtolower(trim($send_as_is_raw));
|
||||
if (!in_array($normalized_send_as_is, ['true', 'false', '1', '0'], true)) {
|
||||
$response->error('send_as_is must be a boolean', 400);
|
||||
}
|
||||
$send_as_is = in_array($normalized_send_as_is, ['true', '1'], true);
|
||||
} else {
|
||||
$response->error('send_as_is must be a boolean', 400);
|
||||
}
|
||||
}
|
||||
// Validate the ID against the database
|
||||
|
||||
$collected_order_invoices = (new collected_order_invoices_o())->select((int)self::getParameter('id'));
|
||||
$collected_order_invoices->requireSelected();
|
||||
// Check if the collected order invoice has an external ID
|
||||
if ($collected_order_invoices->external_id->value() === null) {
|
||||
// Check if the send_as_is parameter is set to true
|
||||
if (!$send_as_is) {
|
||||
// Add fixed price to the collected order invoice
|
||||
$customer_fixed_pricing_o = new customer_fixed_pricing_o();
|
||||
if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
|
||||
$customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
|
||||
$customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
|
||||
$collected_order_invoices->overridePricesFixed((int)$customer_fixed_pricing_price);
|
||||
} else {
|
||||
// Apply vehicle subscriptions if the customer does not have fixed pricing
|
||||
$collected_order_invoices->addVehicleSubscriptionsTransaction();
|
||||
}
|
||||
} else {
|
||||
// Since we are sending the invoice as is, we need to check if the invoice has any left-over subscription / fixed price transactions
|
||||
$collected_order_invoices->removeSpecialArrangements(); // Remove any left-over special arrangement transactions (subscriptions / fixed price)
|
||||
// Reset the price of all items set not to be included in the invoice
|
||||
//$collected_order_invoices->resetPricesOfItemsNotIncludedInInvoice();
|
||||
// Set all items to be included in the invoice
|
||||
$collected_order_invoices->setAllItemsToBeIncludedInInvoice(); // Set all items to be included in the invoice, since we are sending the invoice as is.
|
||||
}
|
||||
// Add the collected order invoice to E-Conomic
|
||||
$collected_order_invoices->addToEconomic();
|
||||
$response->success($collected_order_invoices->asArray());
|
||||
}
|
||||
// Check if the invoice has been booked
|
||||
|
||||
if ($collected_order_invoices->booked_invoice_id->value() !== null) {
|
||||
$response->error('Invoice has already been booked', 400);
|
||||
}
|
||||
// Check if the invoice draft exists in E-Conomic
|
||||
if ($collected_order_invoices->isDraftExisting()) {
|
||||
$response->error('Invoice draft already exists in E-Conomic', 400);
|
||||
|
||||
if (!$this->isEconomicTransferQueueAvailable()) {
|
||||
try {
|
||||
$result = $this->exportCollectedInvoiceSynchronously($collected_order_invoices, $send_as_is);
|
||||
} catch (\Throwable $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
|
||||
(new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_ECONOMIC', 'Processed collected invoice transfer synchronously (queue unavailable)');
|
||||
$response->success($result);
|
||||
}
|
||||
// Add fixed price to the collected order invoice
|
||||
$customer_fixed_pricing_o = new customer_fixed_pricing_o();
|
||||
if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
|
||||
$customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
|
||||
$customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
|
||||
$collected_order_invoices->overridePricesFixed((int)$customer_fixed_pricing_price);
|
||||
} else {
|
||||
// Apply vehicle subscriptions if the customer does not have fixed pricing
|
||||
$collected_order_invoices->addVehicleSubscriptionsTransaction();
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->enqueue(
|
||||
economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT,
|
||||
[
|
||||
'collected_invoice_id' => (int)self::getParameter('id'),
|
||||
'send_as_is' => $send_as_is,
|
||||
'requested_by' => (int)$user->id,
|
||||
],
|
||||
(int)$user->id
|
||||
);
|
||||
$job_id = (int)($job['id'] ?? 0);
|
||||
if ($job_id < 1) {
|
||||
$response->error('Failed to enqueue collected invoice export job: missing queue job id in enqueue response', 500);
|
||||
}
|
||||
// Create the invoice in E-Conomic
|
||||
$collected_order_invoices->addToEconomic(true);
|
||||
// Return the collected order invoice
|
||||
$response->success($collected_order_invoices->asArray());
|
||||
|
||||
(new logs_o())->add('orderInvoices', 'global', 1, $user->id, 'ADD_COLLECTED_INVOICE_ECONOMIC_QUEUED', 'Queued collected invoice transfer to E-Conomic');
|
||||
$response->success([
|
||||
'message' => 'Collected invoice export queued',
|
||||
'job_id' => $job_id,
|
||||
'job' => $job,
|
||||
], 202);
|
||||
} else {
|
||||
(new logs_o())->add('orderInvoices', 'global', 0, 0, 'ADD_COLLECTED_INVOICE_ECONOMIC', 'User tried to add a collected order invoice to E-Conomic without a valid session');
|
||||
$response->error('Invalid session', 400);
|
||||
@@ -603,6 +603,232 @@ class orderInvoicesRoute
|
||||
]
|
||||
);
|
||||
|
||||
$this->get('/collected-invoices/economic/queue', function () {
|
||||
global $response;
|
||||
self::requirePermission('add_collected_invoice_economic');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
$statuses = [];
|
||||
if (self::isParametersSet(['status'])) {
|
||||
$status_raw = (string)self::getParameter('status');
|
||||
$statuses = array_values(array_filter(array_map('trim', explode(',', $status_raw))));
|
||||
}
|
||||
$limit = self::isParametersSet(['limit']) ? (int)self::getParameter('limit') : 50;
|
||||
$offset = self::isParametersSet(['offset']) ? (int)self::getParameter('offset') : 0;
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$jobs = $queue->listJobs($statuses, $limit, $offset);
|
||||
$filtered = array_values(array_filter($jobs, static function (array $job): bool {
|
||||
return ($job['transfer_type'] ?? null) === economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT;
|
||||
}));
|
||||
|
||||
$response->success([
|
||||
'items' => $filtered,
|
||||
'count' => count($filtered),
|
||||
]);
|
||||
},
|
||||
[
|
||||
'add_collected_invoice_economic' => 'List queued collected invoice transfer jobs.'
|
||||
]
|
||||
);
|
||||
|
||||
$this->get('/collected-invoices/economic/queue/status', function () {
|
||||
global $response;
|
||||
self::requirePermission('add_collected_invoice_economic');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
self::requireParameters(['job_id']);
|
||||
self::requireType((int)self::getParameter('job_id'), self::type_int());
|
||||
self::requireMinValue((int)self::getParameter('job_id'), 1);
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->getJobById((int)self::getParameter('job_id'));
|
||||
if ($job === null || ($job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT) {
|
||||
$response->error('Collected invoice queue job not found', 404);
|
||||
}
|
||||
$response->success($job);
|
||||
},
|
||||
[
|
||||
'add_collected_invoice_economic' => 'Get queued collected invoice transfer job status.'
|
||||
]
|
||||
);
|
||||
|
||||
$this->post('/collected-invoices/economic/queue/retry', function () {
|
||||
global $response;
|
||||
self::requirePermission('add_collected_invoice_economic');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
$this->ensureEconomicTransferQueueIsAvailable();
|
||||
|
||||
self::requireParameters(['job_id']);
|
||||
self::requireType((int)self::getParameter('job_id'), self::type_int());
|
||||
self::requireMinValue((int)self::getParameter('job_id'), 1);
|
||||
|
||||
$queue = new economic_transfer_queue();
|
||||
$job = $queue->getJobById((int)self::getParameter('job_id'));
|
||||
if ($job === null || ($job['transfer_type'] ?? null) !== economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT) {
|
||||
$response->error('Collected invoice queue job not found', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$retried = $queue->retryJob((int)self::getParameter('job_id'));
|
||||
} catch (\Throwable $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$response->success([
|
||||
'message' => 'Collected invoice queue job retried',
|
||||
'job' => $retried,
|
||||
]);
|
||||
},
|
||||
[
|
||||
'add_collected_invoice_economic' => 'Retry failed queued collected invoice transfer job.'
|
||||
]
|
||||
);
|
||||
|
||||
/** Collected order invoices > Move multiple > Registration numbers > POST */
|
||||
$this->post('/collected-invoices/move-multiple/registration-numbers', function () {
|
||||
global $response;
|
||||
self::requirePermission('move_collected_invoice');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
self::requireParameters(['registration_numbers', 'target_customer_number', 'from_date', 'to_date', 'closed_at']);
|
||||
$registration_numbers = self::getParameter('registration_numbers');
|
||||
if (!is_array($registration_numbers) || empty($registration_numbers)) {
|
||||
$response->error('registration_numbers must be a non-empty array', 400);
|
||||
}
|
||||
$target_customer_number = self::getParameter('target_customer_number');
|
||||
self::requireType((int)$target_customer_number, self::type_int());
|
||||
self::requireMinValue((int)$target_customer_number, 1);
|
||||
$closed_at = self::getParameter('closed_at');
|
||||
if (!!$closed_at) {
|
||||
self::requireDateFormat($closed_at, self::FORMAT_DATE());
|
||||
$closed_at = date('Y-m-d H:i:s', strtotime($closed_at . ' 23:59:59'));
|
||||
} else {
|
||||
$closed_at = null;
|
||||
}
|
||||
$from_date = self::getParameter('from_date');
|
||||
self::requireDateFormat($from_date, self::FORMAT_DATE());
|
||||
$to_date = self::getParameter('to_date');
|
||||
self::requireDateFormat($to_date, self::FORMAT_DATE());
|
||||
$from_date = date('Y-m-d 00:00:00', strtotime($from_date));
|
||||
$to_date = date('Y-m-d 23:59:59', strtotime($to_date));
|
||||
if (strtotime($from_date) > strtotime($to_date)) {
|
||||
$response->error('from_date must be before or equal to to_date', 400);
|
||||
}
|
||||
$customer = (new users_o())->getUserByCustomerNumber((int)$target_customer_number);
|
||||
$customer->requireSelected();
|
||||
|
||||
$collected_order_invoices = new collected_order_invoices_o();
|
||||
$moved_invoices = [];
|
||||
// Create a new collected order invoice for the target customer
|
||||
$new_invoice_collection = $collected_order_invoices->add(
|
||||
$customer->customer_number->value(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$closed_at
|
||||
);
|
||||
if (!$new_invoice_collection->exists()) {
|
||||
$response->error('Failed to create new invoice collection');
|
||||
}
|
||||
|
||||
foreach ($registration_numbers as $registration_number) {
|
||||
if (!is_string($registration_number) || trim($registration_number) === '') {
|
||||
$response->error('Each registration number must be a non-empty string', 400);
|
||||
}
|
||||
$orders = (new orders_o())->getOrdersWithRegistrationNumberInDateRange($registration_number, $from_date, $to_date);
|
||||
if (empty($orders)) {
|
||||
continue; // Skip if no orders found for this registration number in the date range
|
||||
}
|
||||
foreach ($orders as $order) {
|
||||
$order->customer_id->set((int)$new_invoice_collection->customer_number->value());
|
||||
$order->assignToInvoiceCollection((int)$new_invoice_collection->id);
|
||||
$order->objectChanged();
|
||||
$moved_invoices[] = $order->id;
|
||||
}
|
||||
}
|
||||
$response->success([
|
||||
'message' => 'Orders moved to new invoice collection',
|
||||
'moved_invoices' => $moved_invoices,
|
||||
'from_date' => $from_date,
|
||||
'to_date' => $to_date,
|
||||
'target_customer_number' => $target_customer_number,
|
||||
]);
|
||||
});
|
||||
/** Collected order invoices > Move multiple > POST */
|
||||
$this->post('/collected-invoices/move-multiple', function () {
|
||||
// Used to move multiple orders, to a new collected order invoice, in one request, instead of having to move each order one by one
|
||||
global $response;
|
||||
self::requirePermission('move_collected_invoice');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
self::requireParameters(['order_ids', 'target_customer_number', 'closed_at']);
|
||||
$order_ids = self::getParameter('order_ids');
|
||||
if (!is_array($order_ids) || empty($order_ids)) {
|
||||
$response->error('order_ids must be a non-empty array', 400);
|
||||
}
|
||||
$target_customer_number = self::getParameter('target_customer_number');
|
||||
self::requireType((int)$target_customer_number, self::type_int());
|
||||
self::requireMinValue((int)$target_customer_number, 1);
|
||||
$closed_at = self::getParameter('closed_at');
|
||||
if (!!$closed_at) {
|
||||
self::requireDateFormat($closed_at, self::FORMAT_DATE());
|
||||
$closed_at = date('Y-m-d H:i:s', strtotime($closed_at . ' 23:59:59'));
|
||||
} else {
|
||||
$closed_at = null;
|
||||
}
|
||||
$customer = (new users_o())->select((int)$target_customer_number);
|
||||
$customer->requireSelected();
|
||||
|
||||
$collected_order_invoices = new collected_order_invoices_o();
|
||||
$moved_invoices = [];
|
||||
// Create a new collected order invoice for the target customer
|
||||
$new_invoice_collection = $collected_order_invoices->add(
|
||||
$customer->id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$closed_at
|
||||
);
|
||||
if (!$new_invoice_collection->exists()) {
|
||||
$response->error('Failed to create new invoice collection');
|
||||
}
|
||||
foreach ($order_ids as $order_id) {
|
||||
self::requireType((int)$order_id, self::type_int());
|
||||
self::requireMinValue((int)$order_id, 1);
|
||||
$order = (new orders_o())->select((int)$order_id);
|
||||
$order->requireSelected();
|
||||
$order->customer_id->set((int)$new_invoice_collection->customer_number->value());
|
||||
$order->objectChanged();
|
||||
$order->assignToInvoiceCollection((int)$new_invoice_collection->id);
|
||||
$moved_invoices[] = $order->id;
|
||||
}
|
||||
$new_invoice_collection->clearCachedData();
|
||||
$response->success([
|
||||
'message' => 'Orders moved to new invoice collection',
|
||||
'moved_invoices' => $moved_invoices,
|
||||
]);
|
||||
},
|
||||
[
|
||||
'move_collected_invoice' => 'Move multiple orders to a new collected order invoice. This is a superuser-only route.'
|
||||
]
|
||||
);
|
||||
|
||||
/** Collected order invoices > E-Conomic > Unlink and clear cached data > POST */
|
||||
$this->post('/collected-invoices/economic/unlink', function () {
|
||||
global $response;
|
||||
@@ -1461,6 +1687,68 @@ class orderInvoicesRoute
|
||||
return json_decode(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous fallback when queue components are unavailable in this deployment.
|
||||
* Mirrors the collected-invoice flow used by the queue executor.
|
||||
* @throws Exception
|
||||
*/
|
||||
private function exportCollectedInvoiceSynchronously(collected_order_invoices_o $collected_order_invoices, bool $send_as_is): array
|
||||
{
|
||||
if ($collected_order_invoices->external_id->value() === null) {
|
||||
if (!$send_as_is) {
|
||||
$customer_fixed_pricing_o = new customer_fixed_pricing_o();
|
||||
if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
|
||||
$customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
|
||||
$customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
|
||||
$collected_order_invoices->overridePricesFixed($customer_fixed_pricing_price);
|
||||
} else {
|
||||
$collected_order_invoices->addVehicleSubscriptionsTransaction();
|
||||
}
|
||||
} else {
|
||||
$collected_order_invoices->removeSpecialArrangements();
|
||||
$collected_order_invoices->setAllItemsToBeIncludedInInvoice();
|
||||
}
|
||||
|
||||
$collected_order_invoices->addToEconomic();
|
||||
} else {
|
||||
if ($collected_order_invoices->booked_invoice_id->value() !== null) {
|
||||
throw new Exception('Invoice has already been booked');
|
||||
}
|
||||
if ($collected_order_invoices->isDraftExisting()) {
|
||||
throw new Exception('Invoice draft already exists in E-Conomic');
|
||||
}
|
||||
|
||||
$customer_fixed_pricing_o = new customer_fixed_pricing_o();
|
||||
if ($customer_fixed_pricing_o->doesUserHaveFixedPricing((int)$collected_order_invoices->customer_number->value())) {
|
||||
$customer_fixed_pricing_price = $customer_fixed_pricing_o->selectByCustomerNumber((int)$collected_order_invoices->customer_number->value());
|
||||
$customer_fixed_pricing_price = (int)$customer_fixed_pricing_price->price->value();
|
||||
$collected_order_invoices->overridePricesFixed($customer_fixed_pricing_price);
|
||||
} else {
|
||||
$collected_order_invoices->addVehicleSubscriptionsTransaction();
|
||||
}
|
||||
|
||||
$collected_order_invoices->addToEconomic(true);
|
||||
}
|
||||
|
||||
return $collected_order_invoices->asArray();
|
||||
}
|
||||
|
||||
private function isEconomicTransferQueueAvailable(): bool
|
||||
{
|
||||
return class_exists('\\classes\\economic_transfer_executor')
|
||||
&& class_exists('\\classes\\economic_transfer_queue_schema_bootstrap')
|
||||
&& class_exists(economic_transfer_queue::class);
|
||||
}
|
||||
|
||||
private function ensureEconomicTransferQueueIsAvailable(): void
|
||||
{
|
||||
global $response;
|
||||
|
||||
if (!$this->isEconomicTransferQueueAvailable()) {
|
||||
$response->error('Economic transfer queue is unavailable in this deployment', 503);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $collected_order_invoice
|
||||
* @param users_o $users
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
it('skips zero-cost and zero-quantity items in legacy economic draft helper', function (): void {
|
||||
$content = file_get_contents(app_path('modules/economic/helpers/economic_invoice_draft.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('private function shouldSkipOrderItemLine(array $order_item): bool');
|
||||
expect($content)->toContain('if ($this->shouldSkipOrderItemLine($order_item))');
|
||||
expect($content)->toContain('continue;');
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
app_require('classes/economic_transfer_executor.php');
|
||||
|
||||
use classes\economic_transfer_executor;
|
||||
|
||||
it('skips order items with zero quantity for e-conomic export', function (): void {
|
||||
$shouldSkip = economic_transfer_executor::shouldSkipOrderItemForInvoice([
|
||||
'quantity' => 0,
|
||||
'price' => 199,
|
||||
], 0);
|
||||
|
||||
expect($shouldSkip)->toBeTrue();
|
||||
});
|
||||
|
||||
it('skips order items with zero price for e-conomic export', function (): void {
|
||||
$shouldSkip = economic_transfer_executor::shouldSkipOrderItemForInvoice([
|
||||
'quantity' => 2,
|
||||
'price' => 0,
|
||||
], 2);
|
||||
|
||||
expect($shouldSkip)->toBeTrue();
|
||||
});
|
||||
|
||||
it('keeps positive quantity and price items billable for e-conomic export', function (): void {
|
||||
$shouldSkip = economic_transfer_executor::shouldSkipOrderItemForInvoice([
|
||||
'quantity' => 2,
|
||||
'price' => 149,
|
||||
], 2);
|
||||
|
||||
expect($shouldSkip)->toBeFalse();
|
||||
});
|
||||
|
||||
it('skips malformed order-item payloads for e-conomic export safety', function (): void {
|
||||
expect(economic_transfer_executor::shouldSkipOrderItemForInvoice(null, 1))->toBeTrue();
|
||||
expect(economic_transfer_executor::shouldSkipOrderItemForInvoice('invalid', 1))->toBeTrue();
|
||||
expect(economic_transfer_executor::shouldSkipOrderItemForInvoice([
|
||||
'quantity' => 1,
|
||||
], 1))->toBeTrue();
|
||||
expect(economic_transfer_executor::shouldSkipOrderItemForInvoice([
|
||||
'quantity' => 1,
|
||||
'price' => 'abc',
|
||||
], 1))->toBeTrue();
|
||||
});
|
||||
|
||||
it('wires zero-cost and zero-quantity skip guard into transfer line builder', function (): void {
|
||||
$content = file_get_contents(app_path('classes/economic_transfer_executor.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('if (self::shouldSkipOrderItemForInvoice($order_item, $quantity))');
|
||||
expect($content)->toContain("throw new Exception('No billable order items found');");
|
||||
expect($content)->toContain("throw new Exception('Order item is missing economic product id');");
|
||||
});
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
it('guards all economic invoice queue endpoints before constructing queue service', function (): void {
|
||||
$content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
|
||||
$guardCount = preg_match_all('/\$this->ensureEconomicTransferQueueIsAvailable\(\);/', (string)$content);
|
||||
$queueInitCount = preg_match_all('/new economic_transfer_queue\(\);/', (string)$content);
|
||||
expect($guardCount)->toBe(6);
|
||||
expect($queueInitCount)->toBe(6);
|
||||
|
||||
$endpointPatterns = [
|
||||
"/\\\$this->post\\('\\/economic\\/invoice\\/draft\\/export'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->post\\('\\/economic\\/invoice\\/export'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->get\\('\\/economic\\/invoice\\/draft\\/export\\/status'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->post\\('\\/economic\\/invoice\\/draft\\/export\\/retry'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->get\\('\\/economic\\/invoice\\/export\\/status'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->post\\('\\/economic\\/invoice\\/export\\/retry'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
];
|
||||
|
||||
foreach ($endpointPatterns as $pattern) {
|
||||
expect(preg_match($pattern, (string)$content))->toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('guards all collected-invoice queue endpoints before constructing queue service', function (): void {
|
||||
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
|
||||
$guardCount = preg_match_all('/\$this->ensureEconomicTransferQueueIsAvailable\(\);/', (string)$content);
|
||||
$queueInitCount = preg_match_all('/new economic_transfer_queue\(\);/', (string)$content);
|
||||
expect($guardCount)->toBe(3);
|
||||
expect($queueInitCount)->toBe(4);
|
||||
|
||||
$endpointPatterns = [
|
||||
"/\\\$this->get\\('\\/collected-invoices\\/economic\\/queue'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->get\\('\\/collected-invoices\\/economic\\/queue\\/status'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
"/\\\$this->post\\('\\/collected-invoices\\/economic\\/queue\\/retry'.*?\\\$this->ensureEconomicTransferQueueIsAvailable\\(\\);.*?new economic_transfer_queue\\(\\);/s",
|
||||
];
|
||||
|
||||
foreach ($endpointPatterns as $pattern) {
|
||||
expect(preg_match($pattern, (string)$content))->toBe(1);
|
||||
}
|
||||
|
||||
expect($content)->toContain('if (!$this->isEconomicTransferQueueAvailable()) {');
|
||||
expect($content)->toContain('$result = $this->exportCollectedInvoiceSynchronously($collected_order_invoices, $send_as_is);');
|
||||
expect($content)->toContain('private function exportCollectedInvoiceSynchronously(collected_order_invoices_o $collected_order_invoices, bool $send_as_is): array');
|
||||
expect($content)->toContain('private function isEconomicTransferQueueAvailable(): bool');
|
||||
});
|
||||
|
||||
it('uses a consistent unavailable-service contract for missing queue dependencies', function (): void {
|
||||
$invoiceRouteContent = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
|
||||
$orderInvoicesRouteContent = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
|
||||
|
||||
expect($invoiceRouteContent)->not->toBeFalse();
|
||||
expect($orderInvoicesRouteContent)->not->toBeFalse();
|
||||
|
||||
expect($invoiceRouteContent)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
|
||||
expect($invoiceRouteContent)->toContain("!class_exists('\\\\classes\\\\economic_transfer_executor')");
|
||||
expect($invoiceRouteContent)->toContain("!class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
|
||||
expect($invoiceRouteContent)->toContain('!class_exists(economic_transfer_queue::class)');
|
||||
|
||||
expect($orderInvoicesRouteContent)->toContain('private function isEconomicTransferQueueAvailable(): bool');
|
||||
expect($orderInvoicesRouteContent)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
|
||||
expect($orderInvoicesRouteContent)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
|
||||
expect($orderInvoicesRouteContent)->toContain('class_exists(economic_transfer_queue::class)');
|
||||
expect($orderInvoicesRouteContent)->toContain('if (!$this->isEconomicTransferQueueAvailable()) {');
|
||||
|
||||
foreach ([$invoiceRouteContent, $orderInvoicesRouteContent] as $content) {
|
||||
expect($content)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
|
||||
expect($content)->toContain("Economic transfer queue is unavailable in this deployment', 503");
|
||||
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
|
||||
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue_schema_bootstrap.php';");
|
||||
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue.php';");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
it('registers economic transfer queue cron task and handler', function (): void {
|
||||
$content = file_get_contents(app_path('cron/Cron.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
|
||||
expect($content)->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue_schema_bootstrap.php';");
|
||||
expect($content)->toContain("require_once __DIR__ . '/../classes/economic_transfer_queue.php';");
|
||||
expect($content)->toContain("'EconomicTransferQueueCron'");
|
||||
expect($content)->toContain("'function' => 'EconomicTransferQueueCron'");
|
||||
expect($content)->toContain('function EconomicTransferQueueCron(): void');
|
||||
expect($content)->toContain('new economic_transfer_queue()');
|
||||
expect($content)->toContain('processPending(10)');
|
||||
});
|
||||
|
||||
it('wires queue worker class loading for CLI queue action', function (): void {
|
||||
$content = file_get_contents(app_path('cli.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain("require_once __DIR__ . '/classes/economic_transfer_executor.php';");
|
||||
expect($content)->toContain("require_once __DIR__ . '/classes/economic_transfer_queue_schema_bootstrap.php';");
|
||||
expect($content)->toContain("require_once __DIR__ . '/classes/economic_transfer_queue.php';");
|
||||
expect($content)->toContain("case 'economic-transfer-queue':");
|
||||
expect($content)->toContain('new \\classes\\economic_transfer_queue()');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
it('hardens transfer queue with type validation retry caps and stale lock recovery', function (): void {
|
||||
$content = file_get_contents(app_path('classes/economic_transfer_queue.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('private const STALE_PROCESSING_LOCK_SECONDS = 900');
|
||||
expect($content)->toContain('$this->validateTransferType($transfer_type)');
|
||||
expect($content)->toContain('$max_attempts = max(1, min(10, $max_attempts));');
|
||||
expect($content)->toContain('Queue job reached max retry attempts');
|
||||
expect($content)->toContain('AND attempts < max_attempts');
|
||||
expect($content)->toContain('private function releaseStaleProcessingLocks(): void');
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
function economic_transfer_queue_openapi_content_or_skip(): string
|
||||
{
|
||||
$candidates = [];
|
||||
for ($depth = 1; $depth <= 8; $depth++) {
|
||||
$candidates[] = dirname(__DIR__, $depth) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
|
||||
$cwd = getcwd();
|
||||
if (is_string($cwd) && $cwd !== '') {
|
||||
$candidates[] = $cwd . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
$candidates[] = dirname($cwd) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
|
||||
$candidates = array_values(array_unique($candidates));
|
||||
foreach ($candidates as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
$content = file_get_contents($candidate);
|
||||
if ($content !== false) {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test()->markTestSkipped('openapi.yaml is not available in this runtime environment.');
|
||||
}
|
||||
|
||||
it('documents economic transfer queue paths in openapi', function (): void {
|
||||
$content = economic_transfer_queue_openapi_content_or_skip();
|
||||
|
||||
expect($content)->toContain('/collected-invoices/economic:');
|
||||
expect($content)->toContain('/collected-invoices/economic/queue:');
|
||||
expect($content)->toContain('/collected-invoices/economic/queue/status:');
|
||||
expect($content)->toContain('/collected-invoices/economic/queue/retry:');
|
||||
expect($content)->toContain('/economic/invoice/draft/export/status:');
|
||||
expect($content)->toContain('/economic/invoice/draft/export/retry:');
|
||||
expect($content)->toContain('/economic/invoice/export/status:');
|
||||
expect($content)->toContain('/economic/invoice/export/retry:');
|
||||
});
|
||||
|
||||
it('documents economic transfer queue schemas in openapi', function (): void {
|
||||
$content = economic_transfer_queue_openapi_content_or_skip();
|
||||
|
||||
expect($content)->toContain('EconomicTransferQueueStatus:');
|
||||
expect($content)->toContain('EconomicTransferQueueJob:');
|
||||
expect($content)->toContain('EconomicTransferQueueEnqueueResponse:');
|
||||
expect($content)->toContain('EconomicTransferQueueStatusResponse:');
|
||||
expect($content)->toContain('EconomicTransferQueueRetryResponse:');
|
||||
expect($content)->toContain('EconomicTransferQueueListResponse:');
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
it('registers queued economic invoice endpoints in economicInvoiceRoute', function (): void {
|
||||
$content = file_get_contents(app_path('routes/economicInvoiceRoute.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
|
||||
expect($content)->toContain("!class_exists('\\\\classes\\\\economic_transfer_executor')");
|
||||
expect($content)->toContain("!class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
|
||||
expect($content)->toContain('$this->ensureEconomicTransferQueueIsAvailable();');
|
||||
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
|
||||
expect($content)->toContain('/economic/invoice/draft/export');
|
||||
expect($content)->toContain('/economic/invoice/export');
|
||||
expect($content)->toContain('/economic/invoice/draft/export/status');
|
||||
expect($content)->toContain('/economic/invoice/draft/export/retry');
|
||||
expect($content)->toContain('/economic/invoice/export/status');
|
||||
expect($content)->toContain('/economic/invoice/export/retry');
|
||||
expect($content)->toContain('economic_transfer_queue::TYPE_ORDER_DRAFT_EXPORT');
|
||||
expect($content)->toContain('economic_transfer_queue::TYPE_ORDER_INVOICE_EXPORT');
|
||||
expect($content)->toContain("'job_id' => \$job_id");
|
||||
expect($content)->toContain("missing queue job id in enqueue response");
|
||||
});
|
||||
|
||||
it('registers collected-invoice queue endpoints in orderInvoicesRoute', function (): void {
|
||||
$content = file_get_contents(app_path('routes/orderInvoicesRoute.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('private function ensureEconomicTransferQueueIsAvailable(): void');
|
||||
expect($content)->toContain('private function isEconomicTransferQueueAvailable(): bool');
|
||||
expect($content)->toContain("class_exists('\\\\classes\\\\economic_transfer_executor')");
|
||||
expect($content)->toContain("class_exists('\\\\classes\\\\economic_transfer_queue_schema_bootstrap')");
|
||||
expect($content)->toContain('$this->ensureEconomicTransferQueueIsAvailable();');
|
||||
expect($content)->not->toContain("require_once __DIR__ . '/../classes/economic_transfer_executor.php';");
|
||||
expect($content)->toContain('/collected-invoices/economic');
|
||||
expect($content)->toContain('/collected-invoices/economic/queue');
|
||||
expect($content)->toContain('/collected-invoices/economic/queue/status');
|
||||
expect($content)->toContain('/collected-invoices/economic/queue/retry');
|
||||
expect($content)->toContain('economic_transfer_queue::TYPE_COLLECTED_INVOICE_EXPORT');
|
||||
expect($content)->toContain("'job_id' => \$job_id");
|
||||
expect($content)->toContain("missing queue job id in enqueue response");
|
||||
expect($content)->toContain("requirePermission('add_collected_invoice_economic')");
|
||||
expect($content)->toContain('send_as_is must be a boolean');
|
||||
expect($content)->toContain("in_array(\$normalized_send_as_is, ['true', '1'], true)");
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
it('defines economic transfer queue jobs schema bootstrap table and tracking columns', function (): void {
|
||||
$content = file_get_contents(app_path('classes/economic_transfer_queue_schema_bootstrap.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('CREATE TABLE IF NOT EXISTS economic_transfer_queue_jobs');
|
||||
expect($content)->toContain("status VARCHAR(32) NOT NULL DEFAULT 'QUEUED'");
|
||||
expect($content)->toContain('progress_percent TINYINT UNSIGNED NOT NULL DEFAULT 0');
|
||||
expect($content)->toContain('error_message TEXT NULL');
|
||||
expect($content)->toContain('payload_json JSON NOT NULL');
|
||||
expect($content)->toContain('result_json JSON NULL');
|
||||
});
|
||||
|
||||
it('provides queue processor class constants and processing entrypoint', function (): void {
|
||||
$content = file_get_contents(app_path('classes/economic_transfer_queue.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('public const STATUS_QUEUED');
|
||||
expect($content)->toContain('public const STATUS_PROCESSING');
|
||||
expect($content)->toContain('public const STATUS_COMPLETED');
|
||||
expect($content)->toContain('public const STATUS_FAILED');
|
||||
expect($content)->toContain('public const TYPE_ORDER_DRAFT_EXPORT');
|
||||
expect($content)->toContain('public const TYPE_ORDER_INVOICE_EXPORT');
|
||||
expect($content)->toContain('public const TYPE_COLLECTED_INVOICE_EXPORT');
|
||||
expect($content)->toContain('public function processPending(int $limit = 5): array');
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
app_require('objects/orders_o.php');
|
||||
|
||||
use objects\orders_o;
|
||||
|
||||
final class OrdersRegistrationDateRangeDbResultStub
|
||||
{
|
||||
public int $num_rows;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
private array $rows;
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
public function __construct(array $rows)
|
||||
{
|
||||
$this->rows = array_values($rows);
|
||||
$this->num_rows = count($this->rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function fetch_assoc(): ?array
|
||||
{
|
||||
if ($this->rows === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_shift($this->rows);
|
||||
}
|
||||
}
|
||||
|
||||
final class OrdersRegistrationDateRangeDbStub
|
||||
{
|
||||
public string $lastQuery = '';
|
||||
public int $queryCalls = 0;
|
||||
|
||||
public function escape_string(string $value): string
|
||||
{
|
||||
return addslashes($value);
|
||||
}
|
||||
|
||||
public function query(string $sql): OrdersRegistrationDateRangeDbResultStub
|
||||
{
|
||||
$this->queryCalls++;
|
||||
$this->lastQuery = $sql;
|
||||
return new OrdersRegistrationDateRangeDbResultStub([]);
|
||||
}
|
||||
}
|
||||
|
||||
it('applies created_at bounds directly in SQL when filtering orders by registration number', function (): void {
|
||||
$dbStub = new OrdersRegistrationDateRangeDbStub();
|
||||
$hadDb = array_key_exists('db', $GLOBALS);
|
||||
$previousDb = $hadDb ? $GLOBALS['db'] : null;
|
||||
$GLOBALS['db'] = $dbStub;
|
||||
|
||||
try {
|
||||
$orders = (new orders_o())->getOrdersWithRegistrationNumberInDateRange(
|
||||
' ec21235 ',
|
||||
'2025-03-01 00:00:00',
|
||||
'2025-04-30 23:59:59'
|
||||
);
|
||||
|
||||
expect($orders)->toBe([]);
|
||||
expect($dbStub->queryCalls)->toBe(1);
|
||||
expect($dbStub->lastQuery)->toContain("UPPER(TRIM(reg_1)) = 'EC21235'");
|
||||
expect($dbStub->lastQuery)->toContain("UPPER(TRIM(reg_2)) = 'EC21235'");
|
||||
expect($dbStub->lastQuery)->toContain("UPPER(TRIM(reg_3)) = 'EC21235'");
|
||||
expect($dbStub->lastQuery)->toContain("created_at BETWEEN '2025-03-01 00:00:00' AND '2025-04-30 23:59:59'");
|
||||
expect($dbStub->lastQuery)->toContain('AND deleted_at IS NULL');
|
||||
expect($dbStub->lastQuery)->not->toContain('SELECT id, created_at');
|
||||
} finally {
|
||||
if ($hadDb) {
|
||||
$GLOBALS['db'] = $previousDb;
|
||||
} else {
|
||||
unset($GLOBALS['db']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects an inverted date range for registration lookups', function (): void {
|
||||
$dbStub = new OrdersRegistrationDateRangeDbStub();
|
||||
$hadDb = array_key_exists('db', $GLOBALS);
|
||||
$previousDb = $hadDb ? $GLOBALS['db'] : null;
|
||||
$GLOBALS['db'] = $dbStub;
|
||||
|
||||
try {
|
||||
(new orders_o())->getOrdersWithRegistrationNumberInDateRange(
|
||||
'EC21235',
|
||||
'2025-04-30 23:59:59',
|
||||
'2025-03-01 00:00:00'
|
||||
);
|
||||
} finally {
|
||||
if ($hadDb) {
|
||||
$GLOBALS['db'] = $previousDb;
|
||||
} else {
|
||||
unset($GLOBALS['db']);
|
||||
}
|
||||
}
|
||||
})->throws(\Exception::class, 'The start date cannot be after the end date');
|
||||
|
||||
it('returns early when registration number is blank', function (): void {
|
||||
$dbStub = new OrdersRegistrationDateRangeDbStub();
|
||||
$hadDb = array_key_exists('db', $GLOBALS);
|
||||
$previousDb = $hadDb ? $GLOBALS['db'] : null;
|
||||
$GLOBALS['db'] = $dbStub;
|
||||
|
||||
try {
|
||||
$orders = (new orders_o())->getOrdersWithRegistrationNumberInDateRange(
|
||||
' ',
|
||||
'2025-03-01 00:00:00',
|
||||
'2025-04-30 23:59:59'
|
||||
);
|
||||
|
||||
expect($orders)->toBe([]);
|
||||
expect($dbStub->queryCalls)->toBe(0);
|
||||
} finally {
|
||||
if ($hadDb) {
|
||||
$GLOBALS['db'] = $previousDb;
|
||||
} else {
|
||||
unset($GLOBALS['db']);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
it('validates cached autoload paths before returning and clears stale cache entries', function (): void {
|
||||
$content = file_get_contents(app_path('index.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('$cache_key = \'autoload:\' . $class;');
|
||||
expect($content)->toContain('if (is_string($cached) && $cached !== \'\' && is_file($cached)) {');
|
||||
expect($content)->toContain('if ($is_loaded($class)) {');
|
||||
expect($content)->toContain('redis->delete($cache_key);');
|
||||
});
|
||||
Reference in New Issue
Block a user