Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38814545c4 |
@@ -240,7 +240,13 @@ class economic_transfer_executor
|
|||||||
'Queued transfer processed successfully for collected invoice #' . $collected_invoice_id
|
'Queued transfer processed successfully for collected invoice #' . $collected_invoice_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return $collected_order_invoices->asArray();
|
$result = $collected_order_invoices->asArray();
|
||||||
|
$transfer_metrics = $collected_order_invoices->getLastEconomicTransferMetrics();
|
||||||
|
if ($transfer_metrics !== null) {
|
||||||
|
$result['economic_transfer_metrics'] = $transfer_metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+33
-5
@@ -61,19 +61,47 @@ class economic_invoices_draft_endpoint
|
|||||||
* @throws Exception If the request fails
|
* @throws Exception If the request fails
|
||||||
*/
|
*/
|
||||||
public function add_order(int $invoiceDraftId, orders_o $order, string $currency = 'DKK'): void
|
public function add_order(int $invoiceDraftId, orders_o $order, string $currency = 'DKK'): void
|
||||||
|
{
|
||||||
|
$this->add_orders($invoiceDraftId, [$order], $currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add many orders to a draft invoice and flush their lines in batches.
|
||||||
|
*
|
||||||
|
* @param orders_o[] $orders
|
||||||
|
* @return array{order_count:int,orders_with_invoice_lines:int,line_count:int,batch_count:int,batch_sizes:array<int,int>}
|
||||||
|
* @throws Exception If the request fails
|
||||||
|
*/
|
||||||
|
public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500): array
|
||||||
{
|
{
|
||||||
$draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true);
|
$draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true);
|
||||||
// Check if the order includes any items that should be included in the invoice
|
$orders_with_invoice_lines = 0;
|
||||||
if ($order->getIncludeInInvoiceCount() > 0) {
|
|
||||||
|
foreach ( $orders as $order ) {
|
||||||
|
if (!$order instanceof orders_o) {
|
||||||
|
throw new Exception('Order payload must contain orders_o instances');
|
||||||
|
}
|
||||||
|
// Check if the order includes any items that should be included in the invoice
|
||||||
|
if ($order->getIncludeInInvoiceCount() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orders_with_invoice_lines++;
|
||||||
// Add the transaction header (Timestamp, department, etc.)
|
// Add the transaction header (Timestamp, department, etc.)
|
||||||
$draftInvoice->addNewTransactionHeader($order);
|
$draftInvoice->addNewTransactionHeader($order);
|
||||||
// Add the order lines
|
// Add the order lines
|
||||||
$draftInvoice->addOrderItemLines($order);
|
$draftInvoice->addOrderItemLines($order);
|
||||||
// Add an empty line, so the invoice is not empty
|
// Add an empty line, so the invoice is not empty
|
||||||
$draftInvoice->addTextLine('');
|
$draftInvoice->addTextLine('');
|
||||||
// Save the draft invoice lines
|
|
||||||
$draftInvoice->addLines();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$metrics = $draftInvoice->flushLinesInBatches($line_batch_size);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'order_count' => count($orders),
|
||||||
|
'orders_with_invoice_lines' => $orders_with_invoice_lines,
|
||||||
|
...$metrics,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,4 +179,4 @@ class economic_invoices_draft_endpoint
|
|||||||
$draft_invoice->addLines();
|
$draft_invoice->addLines();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use objects\orders_o;
|
|||||||
|
|
||||||
class economic_invoice_draft
|
class economic_invoice_draft
|
||||||
{
|
{
|
||||||
|
public const DEFAULT_LINE_BATCH_SIZE = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Economic draftInvoiceNumber
|
* The Economic draftInvoiceNumber
|
||||||
* @var int $draft_invoice_number
|
* @var int $draft_invoice_number
|
||||||
@@ -110,13 +112,55 @@ class economic_invoice_draft
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the lines to the draft invoice
|
* Add the lines to the draft invoice.
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function addLines(): void
|
public function addLines(): void
|
||||||
|
{
|
||||||
|
$this->flushLinesInBatches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add queued draft lines using chunked requests.
|
||||||
|
*
|
||||||
|
* @return array{line_count:int,batch_count:int,batch_sizes:array<int,int>}
|
||||||
|
*/
|
||||||
|
public function flushLinesInBatches(int $batch_size = self::DEFAULT_LINE_BATCH_SIZE): array
|
||||||
|
{
|
||||||
|
$lines = array_values($this->draft_lines);
|
||||||
|
$line_count = count($lines);
|
||||||
|
if ($line_count === 0) {
|
||||||
|
return [
|
||||||
|
'line_count' => 0,
|
||||||
|
'batch_count' => 0,
|
||||||
|
'batch_sizes' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$batch_size = max(1, $batch_size);
|
||||||
|
$batch_sizes = [];
|
||||||
|
foreach (array_chunk($lines, $batch_size) as $batch) {
|
||||||
|
$this->sendDraftLines($batch);
|
||||||
|
$batch_sizes[] = count($batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->draft_lines = [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'line_count' => $line_count,
|
||||||
|
'batch_count' => count($batch_sizes),
|
||||||
|
'batch_sizes' => $batch_sizes,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pendingLineCount(): int
|
||||||
|
{
|
||||||
|
return count($this->draft_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sendDraftLines(array $draft_lines): object
|
||||||
{
|
{
|
||||||
$economic = new economic();
|
$economic = new economic();
|
||||||
$economic->invoices->draft->add_lines($this->draft_invoice_number, $this->draft_lines);
|
return $economic->invoices->draft->add_lines($this->draft_invoice_number, $draft_lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class collected_order_invoices_o extends db
|
|||||||
public object_property $updated_at;
|
public object_property $updated_at;
|
||||||
public object_property $closed_at;
|
public object_property $closed_at;
|
||||||
public int $economic_wash_subscription_user_id = 1857;
|
public int $economic_wash_subscription_user_id = 1857;
|
||||||
|
private ?array $last_economic_transfer_metrics = null;
|
||||||
/**
|
/**
|
||||||
* The processor types
|
* The processor types
|
||||||
*
|
*
|
||||||
@@ -712,6 +713,7 @@ class collected_order_invoices_o extends db
|
|||||||
*/
|
*/
|
||||||
public function addInvoicesToDraft(bool $skip_check = false): self
|
public function addInvoicesToDraft(bool $skip_check = false): self
|
||||||
{
|
{
|
||||||
|
$this->last_economic_transfer_metrics = null;
|
||||||
// Require the invoice collection to be selected
|
// Require the invoice collection to be selected
|
||||||
self::requireSelected();
|
self::requireSelected();
|
||||||
// Require the invoice collection to be open
|
// Require the invoice collection to be open
|
||||||
@@ -736,10 +738,20 @@ class collected_order_invoices_o extends db
|
|||||||
usort($orders, function ($a, $b) {
|
usort($orders, function ($a, $b) {
|
||||||
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
||||||
});
|
});
|
||||||
// Add the invoices to the invoice draft
|
// Add the invoice lines to the draft in one accumulated batch path.
|
||||||
|
$order_objects = [];
|
||||||
foreach ( $orders as $order ) {
|
foreach ( $orders as $order ) {
|
||||||
self::addInvoiceToDraft($order['id'], true, $draft_id, $currency);
|
$order_object = new orders_o();
|
||||||
|
$order_object->select((int)$order['id']);
|
||||||
|
$order_object->requireSelected();
|
||||||
|
$order_objects[] = $order_object;
|
||||||
}
|
}
|
||||||
|
$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency);
|
||||||
|
$this->last_economic_transfer_metrics = [
|
||||||
|
'draft_invoice_id' => $draft_id,
|
||||||
|
'currency' => (string)$currency,
|
||||||
|
...$metrics,
|
||||||
|
];
|
||||||
// If the customer has the onlyTankCleaning attribute, add the environmental fee & oil fees to the invoice draft
|
// If the customer has the onlyTankCleaning attribute, add the environmental fee & oil fees to the invoice draft
|
||||||
self::addEnvironmentalAndOilFeesToDraft($draft_id, $currency);
|
self::addEnvironmentalAndOilFeesToDraft($draft_id, $currency);
|
||||||
// Object changed
|
// Object changed
|
||||||
@@ -748,6 +760,11 @@ class collected_order_invoices_o extends db
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLastEconomicTransferMetrics(): ?array
|
||||||
|
{
|
||||||
|
return $this->last_economic_transfer_metrics;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the environmental fee & oil fees to the invoice draft, if the customer has the onlyTankCleaning attribute
|
* Add the environmental fee & oil fees to the invoice draft, if the customer has the onlyTankCleaning attribute
|
||||||
* @param int $draft_id The invoice draft id
|
* @param int $draft_id The invoice draft id
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('routes collected invoice draft line uploads through the multi-order batch endpoint', function (): void {
|
||||||
|
$content = file_get_contents(dirname(__DIR__, 3) . '/objects/collected_order_invoices_o.php');
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
$content = (string)$content;
|
||||||
|
|
||||||
|
$start = strpos($content, 'public function addInvoicesToDraft');
|
||||||
|
$end = strpos($content, 'public function getLastEconomicTransferMetrics');
|
||||||
|
expect($start)->not->toBeFalse();
|
||||||
|
expect($end)->not->toBeFalse();
|
||||||
|
expect($end)->toBeGreaterThan($start);
|
||||||
|
|
||||||
|
$methodBlock = substr($content, (int)$start, (int)$end - (int)$start);
|
||||||
|
expect($methodBlock)->toContain('$order_objects = [];')
|
||||||
|
->and($methodBlock)->toContain('$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency);')
|
||||||
|
->and($methodBlock)->toContain('...$metrics')
|
||||||
|
->and($methodBlock)->not->toContain('self::addInvoiceToDraft($order[\'id\'], true, $draft_id, $currency);');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps single-order draft uploads as a wrapper around the batch endpoint', function (): void {
|
||||||
|
$content = file_get_contents(dirname(__DIR__, 3) . '/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php');
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
$content = (string)$content;
|
||||||
|
|
||||||
|
$singleStart = strpos($content, 'public function add_order');
|
||||||
|
$singleEnd = strpos($content, 'public function add_orders');
|
||||||
|
expect($singleStart)->not->toBeFalse();
|
||||||
|
expect($singleEnd)->not->toBeFalse();
|
||||||
|
expect($singleEnd)->toBeGreaterThan($singleStart);
|
||||||
|
|
||||||
|
$singleBlock = substr($content, (int)$singleStart, (int)$singleEnd - (int)$singleStart);
|
||||||
|
expect($singleBlock)->toContain('$this->add_orders($invoiceDraftId, [$order], $currency);');
|
||||||
|
|
||||||
|
$batchBlock = substr($content, (int)$singleEnd);
|
||||||
|
expect($batchBlock)->toContain('$draftInvoice->flushLinesInBatches($line_batch_size);')
|
||||||
|
->and($batchBlock)->toContain("'orders_with_invoice_lines' => \$orders_with_invoice_lines");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes collected invoice batch transfer metrics in queue results when available', function (): void {
|
||||||
|
$content = file_get_contents(dirname(__DIR__, 3) . '/classes/economic_transfer_executor.php');
|
||||||
|
|
||||||
|
expect($content)->not->toBeFalse();
|
||||||
|
$content = (string)$content;
|
||||||
|
|
||||||
|
expect($content)->toContain('$transfer_metrics = $collected_order_invoices->getLastEconomicTransferMetrics();')
|
||||||
|
->and($content)->toContain("\$result['economic_transfer_metrics'] = \$transfer_metrics;");
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use helpers\economic_invoice_draft;
|
||||||
|
|
||||||
|
if (!class_exists('EconomicInvoiceDraftBatchingProbe')) {
|
||||||
|
class EconomicInvoiceDraftBatchingProbe extends economic_invoice_draft
|
||||||
|
{
|
||||||
|
public array $sentBatches = [];
|
||||||
|
|
||||||
|
protected function sendDraftLines(array $draft_lines): object
|
||||||
|
{
|
||||||
|
$this->sentBatches[] = $draft_lines;
|
||||||
|
return (object)['lines' => $draft_lines];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists('EconomicInvoiceDraftFailingBatchingProbe')) {
|
||||||
|
class EconomicInvoiceDraftFailingBatchingProbe extends EconomicInvoiceDraftBatchingProbe
|
||||||
|
{
|
||||||
|
public int $failOnBatch = 1;
|
||||||
|
|
||||||
|
protected function sendDraftLines(array $draft_lines): object
|
||||||
|
{
|
||||||
|
if (count($this->sentBatches) + 1 === $this->failOnBatch) {
|
||||||
|
throw new RuntimeException('Simulated e-conomic line batch failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::sendDraftLines($draft_lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('does not call e-conomic when flushing an empty draft line buffer', function (): void {
|
||||||
|
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||||
|
|
||||||
|
$metrics = $draft->flushLinesInBatches();
|
||||||
|
|
||||||
|
expect($metrics)->toBe([
|
||||||
|
'line_count' => 0,
|
||||||
|
'batch_count' => 0,
|
||||||
|
'batch_sizes' => [],
|
||||||
|
])->and($draft->sentBatches)->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flushes a small draft line buffer in one request and clears pending lines', function (): void {
|
||||||
|
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||||
|
$draft->addTextLine('line-0');
|
||||||
|
$draft->addTextLine('line-1');
|
||||||
|
$draft->addTextLine('line-2');
|
||||||
|
|
||||||
|
$metrics = $draft->flushLinesInBatches(500);
|
||||||
|
|
||||||
|
expect($metrics)->toBe([
|
||||||
|
'line_count' => 3,
|
||||||
|
'batch_count' => 1,
|
||||||
|
'batch_sizes' => [3],
|
||||||
|
])->and($draft->sentBatches)->toHaveCount(1)
|
||||||
|
->and($draft->sentBatches[0][0]['description'])->toBe('line-0')
|
||||||
|
->and($draft->sentBatches[0][2]['description'])->toBe('line-2')
|
||||||
|
->and($draft->pendingLineCount())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chunks large draft line buffers while preserving line order', function (): void {
|
||||||
|
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||||
|
for ($i = 0; $i < 1201; $i++) {
|
||||||
|
$draft->addTextLine('line-' . $i);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metrics = $draft->flushLinesInBatches(500);
|
||||||
|
|
||||||
|
expect($metrics)->toBe([
|
||||||
|
'line_count' => 1201,
|
||||||
|
'batch_count' => 3,
|
||||||
|
'batch_sizes' => [500, 500, 201],
|
||||||
|
])->and($draft->sentBatches)->toHaveCount(3)
|
||||||
|
->and($draft->sentBatches[0][0]['description'])->toBe('line-0')
|
||||||
|
->and($draft->sentBatches[1][0]['description'])->toBe('line-500')
|
||||||
|
->and($draft->sentBatches[2][200]['description'])->toBe('line-1200')
|
||||||
|
->and($draft->pendingLineCount())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bubbles line batch failures and keeps pending lines available', function (): void {
|
||||||
|
$draft = new EconomicInvoiceDraftFailingBatchingProbe(123, 'DKK', true);
|
||||||
|
$draft->addTextLine('line-0');
|
||||||
|
|
||||||
|
expect(fn () => $draft->flushLinesInBatches(500))
|
||||||
|
->toThrow(RuntimeException::class, 'Simulated e-conomic line batch failure');
|
||||||
|
|
||||||
|
expect($draft->sentBatches)->toBe([])
|
||||||
|
->and($draft->pendingLineCount())->toBe(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user