feat: integrate custom OData endpoints for stock and price sync

Replace broken itemLedgerEntries approach with custom ItemByLocation
OData V4 endpoint for location-specific stock. Add ListaPrecios
endpoint for price list sync (B2C regular, B2C_OF sale price) with
filters for active status and all-customers assignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 13:14:15 +01:00
parent 2c36344932
commit 2495b82e66
4 changed files with 292 additions and 38 deletions

View File

@@ -22,6 +22,11 @@ class WBC_API_Client {
*/
const BASE_URL = 'https://api.businesscentral.dynamics.com/v2.0/%s/api/v2.0';
/**
* OData V4 URL template: tenant_id, environment, company_name
*/
const ODATA_URL = 'https://api.businesscentral.dynamics.com/v2.0/%s/%s/ODataV4/Company(\'%s\')';
/**
* Maximum retry attempts
*/
@@ -365,6 +370,121 @@ class WBC_API_Client {
return str_replace( "'", "''", $value );
}
/**
* Build the OData V4 base URL for custom endpoints
*
* @return string|WP_Error Base URL or error if company name not configured.
*/
private static function get_odata_base_url() {
$tenant_id = WBC_OAuth::get_tenant_id();
$environment = WBC_OAuth::get_environment();
$company_name = get_option( 'wbc_company_name', '' );
if ( empty( $company_name ) ) {
return new WP_Error( 'wbc_missing_company_name', __( 'Company Name is not configured (required for OData endpoints).', 'woo-business-central' ) );
}
return sprintf( self::ODATA_URL, $tenant_id, $environment, rawurlencode( $company_name ) );
}
/**
* GET request to a custom OData V4 endpoint
*
* @param string $endpoint OData endpoint name (e.g. 'ItemByLocation').
* @param array $params OData query parameters ($filter, $select, etc.).
* @return array|WP_Error Response data or error.
*/
public static function odata_get( $endpoint, $params = array() ) {
$base_url = self::get_odata_base_url();
if ( is_wp_error( $base_url ) ) {
return $base_url;
}
$url = $base_url . '/' . $endpoint;
if ( ! empty( $params ) ) {
$url .= '?' . http_build_query( $params );
}
// Get access token
$token = WBC_OAuth::get_access_token();
if ( is_wp_error( $token ) ) {
return $token;
}
WBC_Logger::debug( 'API', 'OData GET request', array( 'url' => $url ) );
$ch = curl_init();
curl_setopt_array( $ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::TIMEOUT,
CURLOPT_HTTPGET => true,
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer ' . $token,
'Accept: application/json',
),
) );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$curl_error = curl_error( $ch );
curl_close( $ch );
if ( $response === false ) {
WBC_Logger::error( 'API', 'OData cURL error', array( 'error' => $curl_error, 'url' => $url ) );
return new WP_Error( 'wbc_curl_error', $curl_error );
}
$data = json_decode( $response, true );
// Handle 401 - retry with fresh token
if ( $http_code === 401 ) {
WBC_OAuth::clear_token_cache();
$token = WBC_OAuth::get_access_token( true );
if ( is_wp_error( $token ) ) {
return $token;
}
$ch = curl_init();
curl_setopt_array( $ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::TIMEOUT,
CURLOPT_HTTPGET => true,
CURLOPT_HTTPHEADER => array(
'Authorization: Bearer ' . $token,
'Accept: application/json',
),
) );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
$data = json_decode( $response, true );
}
if ( $http_code >= 400 ) {
$error_message = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'OData request failed', 'woo-business-central' );
WBC_Logger::error( 'API', 'OData request failed', array(
'http_code' => $http_code,
'error' => $data['error'] ?? null,
'url' => $url,
) );
return new WP_Error( 'wbc_odata_error', $error_message, array( 'status' => $http_code ) );
}
WBC_Logger::debug( 'API', 'OData request successful', array( 'http_code' => $http_code, 'url' => $url ) );
return $data;
}
/**
* Get a specific item by number
*