Files
api/services/nginx/app/modules/economic/economic_m.php
T
8735bae8d5 Fix queue export 400 by aligning e-conomic reference payloads (#351)
## Context
Queue job **#3572** failed in `COLLECTED_INVOICE_EXPORT` with e-conomic
HTTP 400 (`Validation failed. 2 errors found.`) while creating draft for
customer `35131752`.

## Fix
- align draft payload optional references to e-conomic object references
(not id-only fragments):
  - `recipient.attention`
  - `references.customerContact`
  - `references.salesPerson`
  - `references.vendorReference` (legacy path)
  - `deliveryLocation`
- include upstream `self` links when available from customer payload
- keep both collected and legacy draft creation paths consistent
- improve e-conomic error formatting so nested annotated validation
errors and `developerHint` are included in thrown messages

## Tests
- `vendor/bin/pest
tests/Unit/Invoicing/EconomicCustomerEanHelperTest.php
tests/Unit/Invoicing/EconomicInvoiceDraftRecipientEanWiringTest.php
tests/Unit/Invoicing/EconomicLegacyDraftPayloadWiringTest.php
tests/Unit/Invoicing/EconomicUpstreamErrorFormattingTest.php
tests/Unit/Invoicing/EconomicLegacyDraftDiscountWiringTest.php
tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php
tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php
tests/Unit/Invoicing/EconomicInvoiceDraftItemizedDiscountTest.php
--colors=never`

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-05 14:04:59 +00:00

230 lines
8.2 KiB
PHP

<?php
use classes\economic;
class economic_m
{
public economic $economic;
/**
* https://secure.e-conomic.com/secure/api1/requestaccess.aspx?appPublicToken=WeG89W2Y63wLjy1lnqfpsHlfa1mBiyNKByqtGmBOoOw&redirectUrl=truckwash.dk
*/
private string $api_url = 'https://restapi.e-conomic.com';
private string $appAccessGrant;
private string $app_token;
private string $appAccessGrant2;
private static bool $reportedMissingRequiredCredentials = false;
private static bool $reportedMissingGrant2 = false;
public function __construct()
{
global $ECONOMIC_API;
$this->app_token = (string)($ECONOMIC_API['app_secret_token'] ?? '');
$this->appAccessGrant = (string)($ECONOMIC_API['app_access_grant'] ?? '');
$this->appAccessGrant2 = (string)($ECONOMIC_API['app_access_grant2'] ?? '');
$this->economic = new economic();
}
protected function resolve_app_secret_token(): string
{
$appSecretToken = trim($this->app_token);
if ($appSecretToken === '') {
if (!self::$reportedMissingRequiredCredentials) {
error_log('[economic_m] Missing ECONOMIC_API_APP_SECRET_TOKEN. e-conomic requests will fail until configuration is fixed.');
self::$reportedMissingRequiredCredentials = true;
}
throw new \RuntimeException('Missing e-conomic app secret token. Set ECONOMIC_API_APP_SECRET_TOKEN and recreate php containers.');
}
return $appSecretToken;
}
protected function resolve_agreement_grant_token(bool $authToken2): string
{
$primaryGrant = trim($this->appAccessGrant);
$secondaryGrant = trim($this->appAccessGrant2);
if ($primaryGrant === '') {
if (!self::$reportedMissingRequiredCredentials) {
error_log('[economic_m] Missing ECONOMIC_API_APP_ACCESS_GRANT. e-conomic requests will fail until configuration is fixed.');
self::$reportedMissingRequiredCredentials = true;
}
throw new \RuntimeException('Missing e-conomic agreement grant token. Set ECONOMIC_API_APP_ACCESS_GRANT and recreate php containers.');
}
if ($authToken2 && $secondaryGrant !== '') {
return $secondaryGrant;
}
if ($authToken2 && $secondaryGrant === '' && !self::$reportedMissingGrant2) {
error_log('[economic_m] ECONOMIC_API_APP_ACCESS_GRANT2 is missing. Falling back to ECONOMIC_API_APP_ACCESS_GRANT.');
self::$reportedMissingGrant2 = true;
}
return $primaryGrant;
}
/**
* Allowed search filters
* @return array
*/
public function allowed_search_filters_customers(): array
{
return [
'address', 'balance', 'barred', 'city', 'corporateIdentificationNumber', 'country', 'creditLimit', 'currency', 'customerGroup.customerGroupNumber', 'customerNumber', 'ean', 'email', 'lastUpdated', 'mobilePhone', 'name', 'publicEntryNumber', 'telephoneAndFaxNumber', 'vatNumber', 'website', 'zip'
];
}
/**
* Send a request to the Economic API
* @param string $url
* @param string $method
* @param string $data
* @param bool $authToken2
* @return string
*/
protected function send_request($url, $method, $data = '', bool $authToken2 = false): string
{
$appSecretToken = $this->resolve_app_secret_token();
$agreementGrantToken = $this->resolve_agreement_grant_token($authToken2);
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $this->api_url . $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => array(
'X-AppSecretToken: ' . $appSecretToken,
'X-AgreementGrantToken: ' . $agreementGrantToken,
'Content-Type: application/json'
),
));
if ($method === 'POST') {
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
$response = curl_exec($curl);
if ($response === false) {
$error = curl_error($curl);
curl_close($curl);
throw new \RuntimeException('Curl error: ' . $error);
}
$httpStatusCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
return $this->assert_successful_response($httpStatusCode, $response);
}
/**
* Validate e-conomic HTTP responses and convert upstream errors into deterministic runtime exceptions.
*/
protected function assert_successful_response(int $httpStatusCode, string|false $response): string
{
if ($response === false) {
throw new \RuntimeException('e-conomic request failed without a response body.');
}
if ($httpStatusCode >= 400) {
throw new \RuntimeException($this->format_upstream_error_message($httpStatusCode, $response));
}
return $response;
}
/**
* Build a sanitized error message from a non-2xx e-conomic response body.
*/
protected function format_upstream_error_message(int $httpStatusCode, string $response): string
{
$prefix = 'e-conomic request failed with HTTP ' . $httpStatusCode;
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
return $prefix . '.';
}
$message = isset($decoded['message']) && is_string($decoded['message'])
? trim($decoded['message'])
: 'Upstream e-conomic error';
$details = [];
if (isset($decoded['errors']) && is_array($decoded['errors'])) {
$flattenedErrors = array_values(array_unique($this->flatten_economic_errors($decoded['errors'], 'errors')));
if (!empty($flattenedErrors)) {
$details['errors'] = $flattenedErrors;
}
}
if (isset($decoded['developerHint']) && is_string($decoded['developerHint'])) {
$developerHint = trim($decoded['developerHint']);
if ($developerHint !== '') {
$details['developerHint'] = $developerHint;
}
}
if (isset($decoded['logId']) && is_scalar($decoded['logId'])) {
$details['logId'] = (string)$decoded['logId'];
}
if (isset($decoded['httpStatusCode']) && is_numeric($decoded['httpStatusCode'])) {
$details['httpStatusCode'] = (int)$decoded['httpStatusCode'];
}
if (empty($details)) {
return $prefix . ': ' . $message;
}
return $prefix
. ': '
. $message
. ' | 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;
}
}