- Implement on-demand warming of manual and automatic flags cache in `invoice_period_flag_service` to handle cache misses effectively. - Extend cURL timeout in `economic_endpoint_t` for improved reliability in network requests.
298 lines
10 KiB
PHP
298 lines
10 KiB
PHP
<?php
|
|
|
|
namespace traits;
|
|
|
|
trait economic_endpoint_t
|
|
{
|
|
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'] ?? '');
|
|
}
|
|
|
|
protected function resolve_app_secret_token(): string
|
|
{
|
|
$appSecretToken = trim($this->app_token);
|
|
if ($appSecretToken === '') {
|
|
if (!self::$reportedMissingRequiredCredentials) {
|
|
error_log('[economic_endpoint_t] 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_endpoint_t] 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_endpoint_t] ECONOMIC_API_APP_ACCESS_GRANT2 is missing. Falling back to ECONOMIC_API_APP_ACCESS_GRANT.');
|
|
self::$reportedMissingGrant2 = true;
|
|
}
|
|
|
|
return $primaryGrant;
|
|
}
|
|
|
|
/**
|
|
* Filters for e-conomic requests
|
|
* @param array $filters
|
|
* @return string The filters for the request (e.g. 'customerNumber$eq:12345678')
|
|
* @example ['customer.customerNumber' => '12345678']
|
|
*/
|
|
public static function filters(array $filters): string
|
|
{
|
|
$filter_string = '';
|
|
foreach ( $filters as $key => $value ) {
|
|
// If the filter string is not empty, and the key has a value, add an $and
|
|
if (!empty($filter_string) && !empty($value)) {
|
|
$filter_string .= '$and:';
|
|
}
|
|
// Check if there's a "$" in the key, and if not, add it as $eq (equals) by default
|
|
if (!str_contains($value, '$') && !empty($value)) {
|
|
$value = '$eq:' . $value;
|
|
}
|
|
$filter_string .= $key . $value;
|
|
}
|
|
return $filter_string;
|
|
}
|
|
|
|
/**
|
|
* Pagination for e-conomic requests
|
|
* @param array $pagination
|
|
* @return string The pagination for the request (e.g. '&maxPageSize=100&skipPages=0')
|
|
* @example ['maxPageSize' => 100, 'skipPages' => 0]
|
|
*/
|
|
public static function pagination(array $pagination): string
|
|
{
|
|
$pagination_string = '';
|
|
foreach ( $pagination as $key => $value ) {
|
|
$pagination_string .= '&' . $key . '=' . $value;
|
|
}
|
|
return $pagination_string;
|
|
}
|
|
|
|
protected function build_request_url_with_base_url(string $base_url, string $url): string
|
|
{
|
|
$path = '/' . ltrim($url, '/');
|
|
$query_separator_position = strpos($path, '?');
|
|
|
|
if ($query_separator_position === false) {
|
|
return $base_url . $path;
|
|
}
|
|
|
|
$path_without_query = substr($path, 0, $query_separator_position);
|
|
$query_string = substr($path, $query_separator_position + 1);
|
|
|
|
if ($query_string === '') {
|
|
return $base_url . $path_without_query;
|
|
}
|
|
|
|
$normalized_pairs = [];
|
|
foreach (explode('&', $query_string) as $pair) {
|
|
if ($pair === '') {
|
|
continue;
|
|
}
|
|
|
|
$parts = explode('=', $pair, 2);
|
|
if (count($parts) === 1) {
|
|
$normalized_pairs[] = rawurlencode(rawurldecode($parts[0]));
|
|
continue;
|
|
}
|
|
|
|
$normalized_pairs[] = rawurlencode(rawurldecode($parts[0]))
|
|
. '='
|
|
. rawurlencode(rawurldecode($parts[1]));
|
|
}
|
|
|
|
return $base_url . $path_without_query . '?' . implode('&', $normalized_pairs);
|
|
}
|
|
|
|
protected function build_request_url(string $url): string
|
|
{
|
|
return $this->build_request_url_with_base_url($this->api_url, $url);
|
|
}
|
|
|
|
protected function create_curl_handle_for_base_url(string $base_url, string $url, string $method, string $data = '', bool $authToken2 = false)
|
|
{
|
|
$appSecretToken = $this->resolve_app_secret_token();
|
|
$agreementGrantToken = $this->resolve_agreement_grant_token($authToken2);
|
|
|
|
$curl = curl_init();
|
|
curl_setopt_array($curl, array(
|
|
CURLOPT_URL => $this->build_request_url_with_base_url($base_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_2TLS,
|
|
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);
|
|
}
|
|
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
|
|
|
return $curl;
|
|
}
|
|
|
|
protected function create_curl_handle(string $url, string $method, string $data = '', bool $authToken2 = false)
|
|
{
|
|
return $this->create_curl_handle_for_base_url($this->api_url, $url, $method, $data, $authToken2);
|
|
}
|
|
|
|
/**
|
|
* Send a request to the Economic API
|
|
* @param string $url
|
|
* @param string $method
|
|
* @param string $data
|
|
* @param bool $authToken2
|
|
* @return string
|
|
*/
|
|
public function send_request($url, $method, $data = '', bool $authToken2 = false): string
|
|
{
|
|
$curl = $this->create_curl_handle($url, $method, $data, $authToken2);
|
|
$response = curl_exec($curl);
|
|
// Check for errors
|
|
if ($response === false) {
|
|
$error = curl_error($curl);
|
|
curl_close($curl);
|
|
throw new \RuntimeException('Curl error: ' . $error);
|
|
}
|
|
if (curl_errno($curl)) {
|
|
$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);
|
|
}
|
|
|
|
public function send_file_download_request($url, $method, $data = '', bool $authToken2 = false, $outputFile = null)
|
|
{
|
|
// Initialize cURL session
|
|
$curl = $this->create_curl_handle($url, $method, $data, $authToken2);
|
|
|
|
// Execute the request
|
|
$response = curl_exec($curl);
|
|
|
|
// Handle errors
|
|
if (curl_errno($curl)) {
|
|
$error = curl_error($curl);
|
|
curl_close($curl);
|
|
throw new \RuntimeException('Curl error: ' . $error);
|
|
}
|
|
|
|
// Close the cURL session
|
|
curl_close($curl);
|
|
|
|
// If a file path is provided, save the response to the file
|
|
if ($outputFile) {
|
|
// Write file contents
|
|
file_put_contents($outputFile, $response);
|
|
return "File saved to: " . $outputFile;
|
|
}
|
|
|
|
// Otherwise, return the raw content (e.g., for inline use)
|
|
return $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'])) {
|
|
$safeErrors = [];
|
|
foreach ( $decoded['errors'] as $error ) {
|
|
if (is_scalar($error)) {
|
|
$safeErrors[] = (string)$error;
|
|
}
|
|
}
|
|
if (!empty($safeErrors)) {
|
|
$details['errors'] = $safeErrors;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|