diff --git a/services/nginx/app/modules/economic/economic_m.php b/services/nginx/app/modules/economic/economic_m.php index 5a8594c7..4ca13c8e 100644 --- a/services/nginx/app/modules/economic/economic_m.php +++ b/services/nginx/app/modules/economic/economic_m.php @@ -155,14 +155,15 @@ class economic_m $details = []; if (isset($decoded['errors']) && is_array($decoded['errors'])) { - $safeErrors = []; - foreach ( $decoded['errors'] as $error ) { - if (is_scalar($error)) { - $safeErrors[] = (string)$error; - } + $flattenedErrors = array_values(array_unique($this->flatten_economic_errors($decoded['errors'], 'errors'))); + if (!empty($flattenedErrors)) { + $details['errors'] = $flattenedErrors; } - if (!empty($safeErrors)) { - $details['errors'] = $safeErrors; + } + if (isset($decoded['developerHint']) && is_string($decoded['developerHint'])) { + $developerHint = trim($decoded['developerHint']); + if ($developerHint !== '') { + $details['developerHint'] = $developerHint; } } @@ -184,4 +185,45 @@ class economic_m . ' | details=' . json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } + + /** + * @return string[] + */ + protected function flatten_economic_errors(mixed $value, string $path = ''): array + { + if (is_string($value)) { + $message = trim($value); + if ($message === '') { + return []; + } + return [$path !== '' ? ($path . ': ' . $message) : $message]; + } + + if (!is_array($value)) { + return []; + } + + $messages = []; + if (isset($value['errorMessage']) && is_string($value['errorMessage'])) { + $errorMessage = trim($value['errorMessage']); + if ($errorMessage !== '') { + $messages[] = $path !== '' ? ($path . ': ' . $errorMessage) : $errorMessage; + } + } + + foreach ($value as $key => $child) { + if ($key === 'errorMessage') { + continue; + } + $segment = is_int($key) ? ('[' . $key . ']') : (string)$key; + $childPath = $path === '' + ? $segment + : (is_int($key) ? ($path . $segment) : ($path . '.' . $segment)); + foreach ($this->flatten_economic_errors($child, $childPath) as $childMessage) { + $messages[] = $childMessage; + } + } + + return $messages; + } } diff --git a/services/nginx/app/modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php b/services/nginx/app/modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php index 8582cf1c..3467e0c6 100644 --- a/services/nginx/app/modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php +++ b/services/nginx/app/modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php @@ -147,27 +147,21 @@ class economic_invoices_drafts_endpoint if ($public_entry_number !== null) { $recipient['publicEntryNumber'] = $public_entry_number; } - $attention_customer_contact_number = $customer->getAttentionCustomerContactNumber(); - if ($attention_customer_contact_number !== null) { - $recipient['attention'] = [ - 'customerContactNumber' => $attention_customer_contact_number, - ]; + $attention_reference = $customer->getAttentionReference(); + if ($attention_reference !== null) { + $recipient['attention'] = $attention_reference; } $references = [ 'other' => $external_id ]; - $reference_customer_contact_number = $customer->getReferenceCustomerContactNumber(); - if ($reference_customer_contact_number !== null) { - $references['customerContact'] = [ - 'customerContactNumber' => $reference_customer_contact_number, - ]; + $customer_contact_reference = $customer->getReferenceCustomerContact(); + if ($customer_contact_reference !== null) { + $references['customerContact'] = $customer_contact_reference; } - $sales_person_employee_number = $customer->getSalesPersonEmployeeNumber(); - if ($sales_person_employee_number !== null) { - $references['salesPerson'] = [ - 'employeeNumber' => $sales_person_employee_number, - ]; + $sales_person_reference = $customer->getSalesPersonReference(); + if ($sales_person_reference !== null) { + $references['salesPerson'] = $sales_person_reference; } $payload = [ @@ -204,11 +198,9 @@ class economic_invoices_drafts_endpoint 'recipient' => $recipient, ]; - $default_delivery_location_number = $customer->getDefaultDeliveryLocationNumber(); - if ($default_delivery_location_number !== null) { - $payload['deliveryLocation'] = [ - 'deliveryLocationNumber' => $default_delivery_location_number, - ]; + $default_delivery_location_reference = $customer->getDefaultDeliveryLocationReference(); + if ($default_delivery_location_reference !== null) { + $payload['deliveryLocation'] = $default_delivery_location_reference; } // Send the request diff --git a/services/nginx/app/modules/economic/helpers/economic_customer.php b/services/nginx/app/modules/economic/helpers/economic_customer.php index 2f8b5fb5..a5c2d4bc 100644 --- a/services/nginx/app/modules/economic/helpers/economic_customer.php +++ b/services/nginx/app/modules/economic/helpers/economic_customer.php @@ -164,31 +164,61 @@ class economic_customer public function getAttentionCustomerContactNumber(): ?int { self::requireSelected(); - return $this->nestedPositiveIntField(['attention', 'customerContactNumber']); + return $this->getAttentionReference()['customerContactNumber'] ?? null; } public function getReferenceCustomerContactNumber(): ?int { self::requireSelected(); - return $this->nestedPositiveIntField(['customerContact', 'customerContactNumber']); + return $this->getReferenceCustomerContact()['customerContactNumber'] ?? null; } public function getSalesPersonEmployeeNumber(): ?int { self::requireSelected(); - return $this->nestedPositiveIntField(['salesPerson', 'employeeNumber']); + return $this->getSalesPersonReference()['employeeNumber'] ?? null; } public function getVendorReferenceEmployeeNumber(): ?int { self::requireSelected(); - return $this->nestedPositiveIntField(['vendorReference', 'employeeNumber']); + return $this->getVendorReferenceReference()['employeeNumber'] ?? null; } public function getDefaultDeliveryLocationNumber(): ?int { self::requireSelected(); - return $this->nestedPositiveIntField(['defaultDeliveryLocation', 'deliveryLocationNumber']); + return $this->getDefaultDeliveryLocationReference()['deliveryLocationNumber'] ?? null; + } + + public function getAttentionReference(): ?array + { + self::requireSelected(); + return $this->nestedReferenceField(['attention'], 'customerContactNumber'); + } + + public function getReferenceCustomerContact(): ?array + { + self::requireSelected(); + return $this->nestedReferenceField(['customerContact'], 'customerContactNumber'); + } + + public function getSalesPersonReference(): ?array + { + self::requireSelected(); + return $this->nestedReferenceField(['salesPerson'], 'employeeNumber'); + } + + public function getVendorReferenceReference(): ?array + { + self::requireSelected(); + return $this->nestedReferenceField(['vendorReference'], 'employeeNumber'); + } + + public function getDefaultDeliveryLocationReference(): ?array + { + self::requireSelected(); + return $this->nestedReferenceField(['defaultDeliveryLocation'], 'deliveryLocationNumber'); } protected function nullableStringField(string $field): ?string @@ -206,6 +236,51 @@ class economic_customer * @param string[] $segments */ protected function nestedPositiveIntField(array $segments): ?int + { + $value = $this->nestedObjectField($segments); + + if (!is_int($value) && !is_float($value) && !is_string($value)) { + return null; + } + + $number = (int)$value; + return $number > 0 ? $number : null; + } + + /** + * @param string[] $segments + */ + protected function nestedReferenceField(array $segments, string $idField): ?array + { + $source = $this->nestedObjectField($segments); + if (!is_object($source)) { + return null; + } + + $idValue = $source->{$idField} ?? null; + if (!is_int($idValue) && !is_float($idValue) && !is_string($idValue)) { + return null; + } + $id = (int)$idValue; + if ($id <= 0) { + return null; + } + + $reference = [ + $idField => $id, + ]; + $self = $source->self ?? null; + if (is_string($self) && trim($self) !== '') { + $reference['self'] = trim($self); + } + + return $reference; + } + + /** + * @param string[] $segments + */ + protected function nestedObjectField(array $segments): mixed { $value = $this->customer_data_object; foreach ($segments as $segment) { @@ -215,12 +290,7 @@ class economic_customer $value = $value->{$segment}; } - if (!is_int($value) && !is_float($value) && !is_string($value)) { - return null; - } - - $number = (int)$value; - return $number > 0 ? $number : null; + return $value; } /** diff --git a/services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php b/services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php index dabd833d..7a42e6fb 100644 --- a/services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php +++ b/services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php @@ -63,31 +63,23 @@ class economic_invoice_draft_mo extends economicInvoicesDrafts if ($public_entry_number !== null) { $recipient['publicEntryNumber'] = $public_entry_number; } - $attention_customer_contact_number = $customer->getAttentionCustomerContactNumber(); - if ($attention_customer_contact_number !== null) { - $recipient['attention'] = [ - 'customerContactNumber' => $attention_customer_contact_number, - ]; + $attention_reference = $customer->getAttentionReference(); + if ($attention_reference !== null) { + $recipient['attention'] = $attention_reference; } $references = []; - $reference_customer_contact_number = $customer->getReferenceCustomerContactNumber(); - if ($reference_customer_contact_number !== null) { - $references['customerContact'] = [ - 'customerContactNumber' => $reference_customer_contact_number, - ]; + $customer_contact_reference = $customer->getReferenceCustomerContact(); + if ($customer_contact_reference !== null) { + $references['customerContact'] = $customer_contact_reference; } - $sales_person_employee_number = $customer->getSalesPersonEmployeeNumber(); - if ($sales_person_employee_number !== null) { - $references['salesPerson'] = [ - 'employeeNumber' => $sales_person_employee_number, - ]; + $sales_person_reference = $customer->getSalesPersonReference(); + if ($sales_person_reference !== null) { + $references['salesPerson'] = $sales_person_reference; } - $vendor_reference_employee_number = $customer->getVendorReferenceEmployeeNumber(); - if ($vendor_reference_employee_number !== null) { - $references['vendorReference'] = [ - 'employeeNumber' => $vendor_reference_employee_number, - ]; + $vendor_reference = $customer->getVendorReferenceReference(); + if ($vendor_reference !== null) { + $references['vendorReference'] = $vendor_reference; } $data = [ @@ -112,11 +104,9 @@ class economic_invoice_draft_mo extends economicInvoicesDrafts $data['references'] = $references; } - $default_delivery_location_number = $customer->getDefaultDeliveryLocationNumber(); - if ($default_delivery_location_number !== null) { - $data['deliveryLocation'] = [ - 'deliveryLocationNumber' => $default_delivery_location_number, - ]; + $default_delivery_location_reference = $customer->getDefaultDeliveryLocationReference(); + if ($default_delivery_location_reference !== null) { + $data['deliveryLocation'] = $default_delivery_location_reference; } return $this->createInvoiceDraft($data); diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicCustomerEanHelperTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicCustomerEanHelperTest.php index 32fcb8d4..62449186 100644 --- a/services/nginx/app/tests/Unit/Invoicing/EconomicCustomerEanHelperTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicCustomerEanHelperTest.php @@ -23,23 +23,48 @@ it('exposes optional EAN and public entry number from fetched e-conomic customer 'publicEntryNumber' => ' DK123456789 ', 'attention' => (object)[ 'customerContactNumber' => 9, + 'self' => 'https://restapi.e-conomic.com/customers/42331123/contacts/9', ], 'customerContact' => (object)[ 'customerContactNumber' => 10, + 'self' => 'https://restapi.e-conomic.com/customers/42331123/contacts/10', ], 'salesPerson' => (object)[ 'employeeNumber' => 12, + 'self' => 'https://restapi.e-conomic.com/employees/12', ], 'vendorReference' => (object)[ 'employeeNumber' => 14, + 'self' => 'https://restapi.e-conomic.com/employees/14', ], 'defaultDeliveryLocation' => (object)[ 'deliveryLocationNumber' => 7, + 'self' => 'https://restapi.e-conomic.com/customers/42331123/delivery-locations/7', ], ]); expect($customer->getEan())->toBe('5790001234567'); expect($customer->getPublicEntryNumber())->toBe('DK123456789'); + expect($customer->getAttentionReference())->toBe([ + 'customerContactNumber' => 9, + 'self' => 'https://restapi.e-conomic.com/customers/42331123/contacts/9', + ]); + expect($customer->getReferenceCustomerContact())->toBe([ + 'customerContactNumber' => 10, + 'self' => 'https://restapi.e-conomic.com/customers/42331123/contacts/10', + ]); + expect($customer->getSalesPersonReference())->toBe([ + 'employeeNumber' => 12, + 'self' => 'https://restapi.e-conomic.com/employees/12', + ]); + expect($customer->getVendorReferenceReference())->toBe([ + 'employeeNumber' => 14, + 'self' => 'https://restapi.e-conomic.com/employees/14', + ]); + expect($customer->getDefaultDeliveryLocationReference())->toBe([ + 'deliveryLocationNumber' => 7, + 'self' => 'https://restapi.e-conomic.com/customers/42331123/delivery-locations/7', + ]); expect($customer->getAttentionCustomerContactNumber())->toBe(9); expect($customer->getReferenceCustomerContactNumber())->toBe(10); expect($customer->getSalesPersonEmployeeNumber())->toBe(12); @@ -56,6 +81,11 @@ it('returns null for blank optional e-conomic customer recipient identifiers', f expect($customer->getEan())->toBeNull(); expect($customer->getPublicEntryNumber())->toBeNull(); + expect($customer->getAttentionReference())->toBeNull(); + expect($customer->getReferenceCustomerContact())->toBeNull(); + expect($customer->getSalesPersonReference())->toBeNull(); + expect($customer->getVendorReferenceReference())->toBeNull(); + expect($customer->getDefaultDeliveryLocationReference())->toBeNull(); expect($customer->getAttentionCustomerContactNumber())->toBeNull(); expect($customer->getReferenceCustomerContactNumber())->toBeNull(); expect($customer->getSalesPersonEmployeeNumber())->toBeNull(); diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftRecipientEanWiringTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftRecipientEanWiringTest.php index 5378591e..2bdeaefb 100644 --- a/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftRecipientEanWiringTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftRecipientEanWiringTest.php @@ -9,13 +9,13 @@ it('wires EAN and public entry number into e-conomic invoice draft recipients', expect($content)->toContain("\$recipient['nemHandelType'] = 'ean';"); expect($content)->toContain('$customer->getPublicEntryNumber()'); expect($content)->toContain("\$recipient['publicEntryNumber']"); - expect($content)->toContain('$customer->getAttentionCustomerContactNumber()'); + expect($content)->toContain('$customer->getAttentionReference()'); expect($content)->toContain("\$recipient['attention']"); - expect($content)->toContain('$customer->getReferenceCustomerContactNumber()'); + expect($content)->toContain('$customer->getReferenceCustomerContact()'); expect($content)->toContain("\$references['customerContact']"); - expect($content)->toContain('$customer->getSalesPersonEmployeeNumber()'); + expect($content)->toContain('$customer->getSalesPersonReference()'); expect($content)->toContain("\$references['salesPerson']"); - expect($content)->toContain('$customer->getDefaultDeliveryLocationNumber()'); + expect($content)->toContain('$customer->getDefaultDeliveryLocationReference()'); expect($content)->toContain("\$payload['deliveryLocation']"); expect($content)->toContain("'recipient' => \$recipient"); }); diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicLegacyDraftPayloadWiringTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicLegacyDraftPayloadWiringTest.php index 10e06dd2..9c283363 100644 --- a/services/nginx/app/tests/Unit/Invoicing/EconomicLegacyDraftPayloadWiringTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicLegacyDraftPayloadWiringTest.php @@ -9,10 +9,15 @@ it('wires legacy draft creation payload with customer e-conomic metadata and EAN expect($content) ->toContain('$customer = $this->economic->getCustomer((int)$this->customer_number);') ->toContain("\$recipient['nemHandelType'] = 'ean';") + ->toContain('$customer->getAttentionReference()') ->toContain("\$recipient['attention']") + ->toContain('$customer->getReferenceCustomerContact()') ->toContain("\$references['customerContact']") + ->toContain('$customer->getSalesPersonReference()') ->toContain("\$references['salesPerson']") + ->toContain('$customer->getVendorReferenceReference()') ->toContain("\$references['vendorReference']") + ->toContain('$customer->getDefaultDeliveryLocationReference()') ->toContain("\$data['deliveryLocation']") ->toContain("'paymentTermsNumber' => (int)\$customer->getPaymentTermsNumber()") ->toContain("'vatZoneNumber' => (int)\$customer->getVatZoneNumber()") diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicUpstreamErrorFormattingTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicUpstreamErrorFormattingTest.php new file mode 100644 index 00000000..e7afd683 --- /dev/null +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicUpstreamErrorFormattingTest.php @@ -0,0 +1,46 @@ +format_upstream_error_message($httpStatusCode, $response); + } + }; + + $response = json_encode([ + 'message' => 'Validation failed. 2 errors found.', + 'developerHint' => 'Review annotated errors for payload details.', + 'errors' => [ + 'recipient' => [ + 'attention' => [ + 'errorMessage' => 'Customer contact not found for recipient attention.', + ], + ], + 'references' => [ + 'customerContact' => [ + 'errorMessage' => 'Customer contact reference is invalid.', + ], + ], + ], + 'logId' => 'abc123', + 'httpStatusCode' => 400, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + expect($response)->not->toBeFalse(); + $formatted = $economic->formatForTest(400, (string)$response); + + expect($formatted) + ->toContain('Validation failed. 2 errors found.') + ->toContain('errors.recipient.attention: Customer contact not found for recipient attention.') + ->toContain('errors.references.customerContact: Customer contact reference is invalid.') + ->toContain('developerHint') + ->toContain('abc123'); +});