Compare commits

..
Author SHA1 Message Date
Jeppe Bundgaard 38814545c4 Optimize collected e-conomic invoice transfers 2026-07-06 11:31:22 +02:00
21 changed files with 263 additions and 329 deletions
@@ -137,10 +137,6 @@ class customer_mass_import_service
if ($cvrLength < 8 || $cvrLength > 20) {
throw new \RuntimeException('CVR must be between 8 and 20 digits.', 400);
}
if ($normalized['ean'] !== null && strlen((string)$normalized['ean']) > 13) {
throw new \RuntimeException('EAN must be at most 13 digits.', 400);
}
}
protected function normalizePositiveInt(mixed $value): ?int
+1 -29
View File
@@ -172,8 +172,7 @@ class economic implements economic_i
string $email,
int $phone,
?int $mobile_phone = null,
object|array|null $company_information = null,
?string $ean = null
object|array|null $company_information = null
): object
{
$payload = [
@@ -197,37 +196,10 @@ class economic implements economic_i
];
$payload = array_replace($payload, $this->buildCustomerPayloadFromCompanyInformation($company_information));
$normalized_ean = self::normalizeCustomerEan($ean);
if ($normalized_ean !== null) {
$payload['ean'] = $normalized_ean;
}
return $this->customers->customers->create($payload);
}
public static function normalizeCustomerEan(mixed $value): ?string
{
if ($value === null) {
return null;
}
$digits = preg_replace('/\D+/', '', (string)$value);
if (!is_string($digits)) {
return null;
}
$digits = trim($digits);
if ($digits === '') {
return null;
}
if (strlen($digits) > 13) {
throw new \InvalidArgumentException('EAN must be at most 13 digits.');
}
return $digits;
}
private function buildCustomerPayloadFromCompanyInformation(object|array|null $company_information): array
{
if ($company_information === null) {
@@ -240,7 +240,13 @@ class economic_transfer_executor
'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;
}
/**
@@ -14,8 +14,6 @@ class economic_customer_mo
public null|string $message;
public null|string $corporateIdentificationNumber;
public null|string $email;
public null|string $ean;
public null|string $publicEntryNumber;
public null|string $mobilePhone;
public null|string $currency;
public null|string $country;
@@ -48,8 +46,6 @@ class economic_customer_mo
$this->zip = ($customer->zip ?? null);
$this->corporateIdentificationNumber = ($customer->corporateIdentificationNumber ?? null);
$this->email = ($customer->email ?? null);
$this->ean = ($customer->ean ?? null);
$this->publicEntryNumber = ($customer->publicEntryNumber ?? $customer->public_entry_number ?? null);
$this->mobilePhone = ($customer->mobilePhone ?? null);
$this->currency = ($customer->currency ?? null);
$this->country = ($customer->country ?? null);
@@ -104,8 +100,6 @@ class economic_customer_mo
'zip' => $this->zip,
'corporateIdentificationNumber' => $this->corporateIdentificationNumber,
'email' => $this->email,
'ean' => $this->ean,
'publicEntryNumber' => $this->publicEntryNumber,
'mobilePhone' => $this->mobilePhone,
'currency' => $this->currency,
'country' => $this->country,
@@ -61,19 +61,47 @@ class economic_invoices_draft_endpoint
* @throws Exception If the request fails
*/
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);
// Check if the order includes any items that should be included in the invoice
if ($order->getIncludeInInvoiceCount() > 0) {
$orders_with_invoice_lines = 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.)
$draftInvoice->addNewTransactionHeader($order);
// Add the order lines
$draftInvoice->addOrderItemLines($order);
// Add an empty line, so the invoice is not empty
$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();
}
}
}
}
@@ -127,23 +127,6 @@ class economic_invoices_drafts_endpoint
$customer_address = $customer->getAddress() ?? 'Ukendt';
$customer_zip = $customer->getZipCode() ?? 'Ukendt';
$customer_city = $customer->getCity() ?? 'Ukendt';
$recipient = [
'name' => $customer_name,
'address' => $customer_address,
'zip' => $customer_zip,
'city' => $customer_city,
'vatZone' => [
'vatZoneNumber' => (int)$customer->getVatZoneNumber(),
],
];
$customer_ean = $customer->getEan();
if ($customer_ean !== null) {
$recipient['ean'] = $customer_ean;
}
$public_entry_number = $customer->getPublicEntryNumber();
if ($public_entry_number !== null) {
$recipient['publicEntryNumber'] = $public_entry_number;
}
// Send the request
$response = $this->send_request(
@@ -182,7 +165,15 @@ class economic_invoices_drafts_endpoint
'currency' => $customer->getCurrency() ?? 'DKK',
// Set the recipient details
'recipient' => $recipient,
'recipient' => [
'name' => $customer_name,
'address' => $customer_address,
'zip' => $customer_zip,
'city' => $customer_city,
'vatZone' => [
'vatZoneNumber' => (int)$customer->getVatZoneNumber(),
],
],
])
);
// Return the response as an object
@@ -203,4 +194,4 @@ class economic_invoices_drafts_endpoint
}
}
}
}
@@ -149,29 +149,6 @@ class economic_customer
return $this->customer_data_object->email;
}
public function getEan(): ?string
{
self::requireSelected();
return $this->nullableStringField('ean');
}
public function getPublicEntryNumber(): ?string
{
self::requireSelected();
return $this->nullableStringField('publicEntryNumber');
}
protected function nullableStringField(string $field): ?string
{
$value = $this->customer_data_object->{$field} ?? null;
if ($value === null) {
return null;
}
$normalized = trim((string)$value);
return $normalized !== '' ? $normalized : null;
}
/**
* Get the customer address
* @return string The customer address
@@ -250,4 +227,4 @@ class economic_customer
return $this->customer_data_object->vatZone->vatZoneNumber;
}
}
}
@@ -10,6 +10,8 @@ use objects\orders_o;
class economic_invoice_draft
{
public const DEFAULT_LINE_BATCH_SIZE = 500;
/**
* The Economic draftInvoiceNumber
* @var int $draft_invoice_number
@@ -110,13 +112,55 @@ class economic_invoice_draft
}
/**
* Add the lines to the draft invoice
* @return void
* Add the lines to the draft invoice.
*/
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->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 $closed_at;
public int $economic_wash_subscription_user_id = 1857;
private ?array $last_economic_transfer_metrics = null;
/**
* The processor types
*
@@ -712,6 +713,7 @@ class collected_order_invoices_o extends db
*/
public function addInvoicesToDraft(bool $skip_check = false): self
{
$this->last_economic_transfer_metrics = null;
// Require the invoice collection to be selected
self::requireSelected();
// Require the invoice collection to be open
@@ -736,10 +738,20 @@ class collected_order_invoices_o extends db
usort($orders, function ($a, $b) {
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 ) {
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
self::addEnvironmentalAndOilFeesToDraft($draft_id, $currency);
// Object changed
@@ -748,6 +760,11 @@ class collected_order_invoices_o extends db
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
* @param int $draft_id The invoice draft id
-12
View File
@@ -2595,12 +2595,6 @@ paths:
type: string
description: Contact person name
example: "Mikkel"
ean:
type: string
description: Optional EAN used for e-invoicing in e-conomic
maxLength: 13
pattern: '^[0-9]{1,13}$'
example: "5790001234567"
g_recaptcha_response:
type: string
description: reCAPTCHA verification token
@@ -8659,12 +8653,6 @@ paths:
email: {type: string}
phone: {type: integer}
name: {type: string}
ean:
type: string
description: Optional EAN used for e-invoicing in e-conomic
maxLength: 13
pattern: '^[0-9]{1,13}$'
example: "5790001234567"
responses:
'200':
description: Success
-9
View File
@@ -427,7 +427,6 @@ class authRoute
$contactEmail = self::getParameter('contactEmail');
$contactPhone = (int)self::getParameter('contactPhone');
$contactName = self::getParameter('contactName');
$ean = null;
/**
* Validate
*/
@@ -455,13 +454,6 @@ class authRoute
self::requireMinValue($contactPhone, 10000000);
self::requireMaxValue($contactPhone, 9999999999);
}
if (self::isParametersSet(['ean'])) {
try {
$ean = economic::normalizeCustomerEan(self::getParameter('ean'));
} catch (\InvalidArgumentException $exception) {
$response->error($exception->getMessage(), 400);
}
}
/**
* If the contact phone is empty, default to company phone
@@ -553,7 +545,6 @@ class authRoute
(int)$companyPhone,
(int)$contactPhone,
$companyInformation,
$ean,
);
} catch (Exception $exception) {
$recoveredCustomer = $this->recoverRegistrationAfterCreateFailure(
@@ -60,14 +60,6 @@ class moduleEconomicCustomerRoute
self::requireMaxLength('phone', 255);
self::requireMinLength('name', 1);
self::requireMaxLength('name', 255);
$ean = null;
if (self::isParametersSet(['ean'])) {
try {
$ean = economic::normalizeCustomerEan(self::getParameter('ean'));
} catch (\InvalidArgumentException $exception) {
$response->error($exception->getMessage(), 400);
}
}
(new logs_o())->add('modules_economic', 'global', 1, 0, 'MODULES_ECONOMIC', 'User accessed the customer');
$result = (new economic())->createCustomer(
(int)self::getParameter('customer_number'),
@@ -75,9 +67,6 @@ class moduleEconomicCustomerRoute
(int)self::getParameter('cvr'),
(string)self::getParameter('email'),
(int)self::getParameter('phone'),
null,
null,
$ean,
);
$response->success((object)$result);
} else {
@@ -87,4 +76,4 @@ class moduleEconomicCustomerRoute
});
}
}
}
@@ -62,7 +62,6 @@ it('returns the raw upstream create response and preserves the requested payload
expect($probe->inner->lastPayload['phone'])->toBe(42331123);
expect($probe->inner->lastPayload['telephoneAndFaxNumber'])->toBe('42331123');
expect($probe->inner->lastPayload['mobilePhone'])->toBe('42331123');
expect(array_key_exists('ean', $probe->inner->lastPayload))->toBeFalse();
});
it('adds supported CVR company fields to the e-conomic customer payload', function (): void {
@@ -98,46 +97,3 @@ it('adds supported CVR company fields to the e-conomic customer payload', functi
expect($probe->inner->lastPayload['mobilePhone'])->toBe('55667788');
expect(array_key_exists('industrycode', $probe->inner->lastPayload))->toBeFalse();
});
it('adds a normalized EAN to the e-conomic customer payload when provided', function (): void {
$stubResponse = (object)[
'customerNumber' => 42331123,
'name' => 'Truckwash ApS',
];
$probe = new EconomicCreateCustomerProbe($stubResponse);
$probe->createCustomer(
42331123,
'Truckwash ApS',
37781258,
'invoice@truckwash.test',
42331123,
null,
null,
'57 90-001234567',
);
expect($probe->inner->lastPayload['ean'])->toBe('5790001234567');
});
it('rejects EAN values longer than e-conomic accepts', function (): void {
$stubResponse = (object)[
'customerNumber' => 42331123,
'name' => 'Truckwash ApS',
];
$probe = new EconomicCreateCustomerProbe($stubResponse);
$call = static fn() => $probe->createCustomer(
42331123,
'Truckwash ApS',
37781258,
'invoice@truckwash.test',
42331123,
null,
null,
'57900012345678',
);
expect($call)->toThrow(InvalidArgumentException::class, 'EAN must be at most 13 digits.');
expect($probe->inner->lastPayload)->toBe([]);
});
@@ -176,22 +176,6 @@ it('creates a new e-conomic customer and returns a created result for new rows',
expect($result['has_account'])->toBeFalse();
});
it('rejects EAN values longer than e-conomic accepts before creating customers', function (): void {
$service = new CustomerMassImportServiceProbe();
$call = static fn() => $service->import([
'cvr' => '29424764',
'name' => 'TGP TRANSPORT APS',
'email' => 'tgp@example.com',
'ean' => '57900012345678',
'phone' => '22725567',
]);
expect($call)->toThrow(RuntimeException::class, 'EAN must be at most 13 digits.');
expect($service->createCalls)->toBe([]);
expect($service->bootstrapCalls)->toBe([]);
});
it('creates the economic record for an existing local account when no matching upstream customer exists', function (): void {
$service = new CustomerMassImportServiceProbe();
$service->localExists = true;
@@ -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;");
});
@@ -1,40 +0,0 @@
<?php
app_require('modules/economic/helpers/economic_customer.php');
use helpers\economic_customer;
function economic_customer_helper_from_payload(object $payload): economic_customer
{
$reflection = new ReflectionClass(economic_customer::class);
/** @var economic_customer $customer */
$customer = $reflection->newInstanceWithoutConstructor();
$property = $reflection->getProperty('customer_data_object');
$property->setAccessible(true);
$property->setValue($customer, $payload);
return $customer;
}
it('exposes optional EAN and public entry number from fetched e-conomic customer data', function (): void {
$customer = economic_customer_helper_from_payload((object)[
'customerNumber' => 42331123,
'ean' => ' 5790001234567 ',
'publicEntryNumber' => ' DK123456789 ',
]);
expect($customer->getEan())->toBe('5790001234567');
expect($customer->getPublicEntryNumber())->toBe('DK123456789');
});
it('returns null for blank optional e-conomic customer recipient identifiers', function (): void {
$customer = economic_customer_helper_from_payload((object)[
'customerNumber' => 42331123,
'ean' => ' ',
'publicEntryNumber' => '',
]);
expect($customer->getEan())->toBeNull();
expect($customer->getPublicEntryNumber())->toBeNull();
});
@@ -1,50 +0,0 @@
<?php
function economic_ean_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';
}
foreach (array_values(array_unique($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.');
}
function economic_ean_openapi_block(string $content, string $start, string $end): string
{
$start_pos = strpos($content, $start);
$end_pos = strpos($content, $end);
expect($start_pos)->not->toBeFalse();
expect($end_pos)->not->toBeFalse();
expect($end_pos)->toBeGreaterThan($start_pos);
return substr($content, (int)$start_pos, (int)$end_pos - (int)$start_pos);
}
it('documents optional EAN on customer creation endpoints', function (): void {
$content = economic_ean_openapi_content_or_skip();
$register_block = economic_ean_openapi_block($content, '/auth/register/cvr:', '/auth/password-reset/request:');
$economic_customer_block = economic_ean_openapi_block($content, '/modules/economic/customer:', '/economic/layouts:');
foreach ([$register_block, $economic_customer_block] as $block) {
expect($block)->toContain('ean:');
expect($block)->toContain('maxLength: 13');
expect($block)->toContain("pattern: '^[0-9]{1,13}$'");
}
});
@@ -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);
});
@@ -1,12 +0,0 @@
<?php
it('wires EAN and public entry number into e-conomic invoice draft recipients', function (): void {
$content = file_get_contents(app_path('modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php'));
expect($content)->not->toBeFalse();
expect($content)->toContain('$customer->getEan()');
expect($content)->toContain("\$recipient['ean']");
expect($content)->toContain('$customer->getPublicEntryNumber()');
expect($content)->toContain("\$recipient['publicEntryNumber']");
expect($content)->toContain("'recipient' => \$recipient");
});
@@ -18,8 +18,6 @@ it('parses cached economic customer payloads that use snake_case customer_number
'customer_number' => '42331123',
'name' => 'Truckwash ApS',
'email' => 'jb@truckwash.dk',
'ean' => '5790001234567',
'public_entry_number' => 'DK123456789',
'currency' => 'DKK',
'country' => 'DK',
'barred' => true,
@@ -33,8 +31,6 @@ it('parses cached economic customer payloads that use snake_case customer_number
'zip' => null,
'corporateIdentificationNumber' => null,
'email' => 'jb@truckwash.dk',
'ean' => '5790001234567',
'publicEntryNumber' => 'DK123456789',
'mobilePhone' => null,
'currency' => 'DKK',
'country' => 'DK',
@@ -105,30 +105,7 @@ namespace classes {
};
}
public static function normalizeCustomerEan(mixed $value): ?string
{
if ($value === null) {
return null;
}
$digits = preg_replace('/\D+/', '', (string)$value);
if (!is_string($digits)) {
return null;
}
$digits = trim($digits);
if ($digits === '') {
return null;
}
if (strlen($digits) > 13) {
throw new \InvalidArgumentException('EAN must be at most 13 digits.');
}
return $digits;
}
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null, $ean = null): object
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null): object
{
self::$create_calls[] = [
'number' => (int)$number,
@@ -138,7 +115,6 @@ namespace classes {
'phone' => (int)$phone,
'mobile_phone' => $mobilePhone === null ? null : (int)$mobilePhone,
'company_information' => $companyInformation,
'ean' => $ean === null ? null : (string)$ean,
];
if (self::$mock_create_exception !== null) {
@@ -448,16 +424,6 @@ namespace {
'expected_error' => 'Parameter cvr must be at least 8 characters long',
'expected_status' => 400,
],
[
'name' => 'Invalid EAN length (too long)',
'params' => array_merge($baseParams, ['ean' => '57900012345678']),
'expected_error' => 'EAN must be at most 13 digits.',
'expected_status' => 400,
'assert' => static function (): void {
assert_true(count(\classes\economic::$create_calls) === 0, 'Invalid EAN must not create e-conomic customers.');
assert_true(count(\classes\email::$sent) === 0, 'Invalid EAN must not send welcome emails.');
},
],
[
'name' => 'CVR lookup failure returns validation error without creating customer',
'params' => array_merge($baseParams, ['cvr' => '11111112']),
@@ -597,7 +563,7 @@ namespace {
],
[
'name' => 'Successful registration bootstraps local user before welcome emails',
'params' => array_merge($baseParams, ['contactPhone' => 87654320, 'ean' => '57 90-001234567']),
'params' => array_merge($baseParams, ['contactPhone' => 87654320]),
'setup' => static function (): void {
\classes\economic::$mock_create_response = (object)[
'customerNumber' => 12345678,
@@ -613,7 +579,6 @@ namespace {
assert_true(count(\classes\economic::$create_calls) === 1, 'Fresh registration must call create exactly once.');
assert_true(\classes\economic::$create_calls[0]['phone'] === 12345678, 'Fresh registration must use the company phone as the e-conomic customer phone.');
assert_true(\classes\economic::$create_calls[0]['mobile_phone'] === 87654320, 'Fresh registration must pass the contact phone as the e-conomic mobile phone.');
assert_true(\classes\economic::$create_calls[0]['ean'] === '5790001234567', 'Fresh registration must pass normalized EAN to e-conomic.');
$companyInformation = \classes\economic::$create_calls[0]['company_information'];
assert_true(is_object($companyInformation), 'Fresh registration must pass CVR company information to e-conomic.');
assert_true($companyInformation->address === 'Demo Street 1', 'Fresh registration must pass the CVR address to e-conomic.');