Fix e-conomic EAN customer transfer
This commit is contained in:
@@ -137,6 +137,10 @@ 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
|
||||
|
||||
@@ -172,7 +172,8 @@ class economic implements economic_i
|
||||
string $email,
|
||||
int $phone,
|
||||
?int $mobile_phone = null,
|
||||
object|array|null $company_information = null
|
||||
object|array|null $company_information = null,
|
||||
?string $ean = null
|
||||
): object
|
||||
{
|
||||
$payload = [
|
||||
@@ -196,10 +197,37 @@ 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) {
|
||||
|
||||
@@ -14,6 +14,8 @@ 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;
|
||||
@@ -46,6 +48,8 @@ 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);
|
||||
@@ -100,6 +104,8 @@ 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,
|
||||
|
||||
+19
-10
@@ -127,6 +127,23 @@ 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(
|
||||
@@ -165,15 +182,7 @@ class economic_invoices_drafts_endpoint
|
||||
'currency' => $customer->getCurrency() ?? 'DKK',
|
||||
|
||||
// Set the recipient details
|
||||
'recipient' => [
|
||||
'name' => $customer_name,
|
||||
'address' => $customer_address,
|
||||
'zip' => $customer_zip,
|
||||
'city' => $customer_city,
|
||||
'vatZone' => [
|
||||
'vatZoneNumber' => (int)$customer->getVatZoneNumber(),
|
||||
],
|
||||
],
|
||||
'recipient' => $recipient,
|
||||
])
|
||||
);
|
||||
// Return the response as an object
|
||||
@@ -194,4 +203,4 @@ class economic_invoices_drafts_endpoint
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,29 @@ 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
|
||||
@@ -227,4 +250,4 @@ class economic_customer
|
||||
return $this->customer_data_object->vatZone->vatZoneNumber;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2595,6 +2595,12 @@ 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
|
||||
@@ -8653,6 +8659,12 @@ 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
|
||||
|
||||
@@ -427,6 +427,7 @@ class authRoute
|
||||
$contactEmail = self::getParameter('contactEmail');
|
||||
$contactPhone = (int)self::getParameter('contactPhone');
|
||||
$contactName = self::getParameter('contactName');
|
||||
$ean = null;
|
||||
/**
|
||||
* Validate
|
||||
*/
|
||||
@@ -454,6 +455,13 @@ 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
|
||||
@@ -545,6 +553,7 @@ class authRoute
|
||||
(int)$companyPhone,
|
||||
(int)$contactPhone,
|
||||
$companyInformation,
|
||||
$ean,
|
||||
);
|
||||
} catch (Exception $exception) {
|
||||
$recoveredCustomer = $this->recoverRegistrationAfterCreateFailure(
|
||||
|
||||
@@ -60,6 +60,14 @@ 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'),
|
||||
@@ -67,6 +75,9 @@ class moduleEconomicCustomerRoute
|
||||
(int)self::getParameter('cvr'),
|
||||
(string)self::getParameter('email'),
|
||||
(int)self::getParameter('phone'),
|
||||
null,
|
||||
null,
|
||||
$ean,
|
||||
);
|
||||
$response->success((object)$result);
|
||||
} else {
|
||||
@@ -76,4 +87,4 @@ class moduleEconomicCustomerRoute
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ 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 {
|
||||
@@ -97,3 +98,46 @@ 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,6 +176,22 @@ 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,40 @@
|
||||
<?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();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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}$'");
|
||||
}
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?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,6 +18,8 @@ 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,
|
||||
@@ -31,6 +33,8 @@ 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,7 +105,30 @@ namespace classes {
|
||||
};
|
||||
}
|
||||
|
||||
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null): object
|
||||
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
|
||||
{
|
||||
self::$create_calls[] = [
|
||||
'number' => (int)$number,
|
||||
@@ -115,6 +138,7 @@ 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) {
|
||||
@@ -424,6 +448,16 @@ 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']),
|
||||
@@ -563,7 +597,7 @@ namespace {
|
||||
],
|
||||
[
|
||||
'name' => 'Successful registration bootstraps local user before welcome emails',
|
||||
'params' => array_merge($baseParams, ['contactPhone' => 87654320]),
|
||||
'params' => array_merge($baseParams, ['contactPhone' => 87654320, 'ean' => '57 90-001234567']),
|
||||
'setup' => static function (): void {
|
||||
\classes\economic::$mock_create_response = (object)[
|
||||
'customerNumber' => 12345678,
|
||||
@@ -579,6 +613,7 @@ 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.');
|
||||
|
||||
Reference in New Issue
Block a user