2026-02-17 09:59:53 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Order Sync to Business Central
|
|
|
|
|
*
|
|
|
|
|
* @package WooBusinessCentral
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Prevent direct access
|
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Class WBC_Order_Sync
|
|
|
|
|
*
|
|
|
|
|
* Handles syncing orders from WooCommerce to Business Central.
|
|
|
|
|
*/
|
|
|
|
|
class WBC_Order_Sync {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Customer sync instance
|
|
|
|
|
*
|
|
|
|
|
* @var WBC_Customer_Sync
|
|
|
|
|
*/
|
|
|
|
|
private $customer_sync;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor
|
|
|
|
|
*/
|
|
|
|
|
public function __construct() {
|
|
|
|
|
$this->customer_sync = new WBC_Customer_Sync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sync an order to Business Central
|
|
|
|
|
*
|
|
|
|
|
* @param int $order_id WooCommerce order ID.
|
|
|
|
|
* @return array|WP_Error Sync result or error.
|
|
|
|
|
*/
|
|
|
|
|
public function sync_order( $order_id ) {
|
|
|
|
|
// Check if order sync is enabled
|
|
|
|
|
if ( get_option( 'wbc_enable_order_sync', 'yes' ) !== 'yes' ) {
|
|
|
|
|
return array(
|
|
|
|
|
'success' => false,
|
|
|
|
|
'message' => __( 'Order sync is disabled.', 'woo-business-central' ),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if credentials are configured
|
|
|
|
|
if ( ! WBC_OAuth::is_configured() ) {
|
|
|
|
|
WBC_Logger::error( 'OrderSync', 'Order sync failed - API credentials not configured', array(
|
|
|
|
|
'order_id' => $order_id,
|
|
|
|
|
) );
|
|
|
|
|
return new WP_Error( 'wbc_not_configured', __( 'API credentials not configured.', 'woo-business-central' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the order
|
|
|
|
|
$order = wc_get_order( $order_id );
|
|
|
|
|
|
|
|
|
|
if ( ! $order ) {
|
|
|
|
|
return new WP_Error( 'wbc_order_not_found', __( 'Order not found.', 'woo-business-central' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if order is already synced
|
|
|
|
|
$bc_order_id = $order->get_meta( '_wbc_bc_order_id' );
|
|
|
|
|
if ( ! empty( $bc_order_id ) ) {
|
|
|
|
|
WBC_Logger::debug( 'OrderSync', 'Order already synced', array(
|
|
|
|
|
'order_id' => $order_id,
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
) );
|
|
|
|
|
return array(
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => __( 'Order already synced to Business Central.', 'woo-business-central' ),
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Acquire a lock to prevent duplicate syncs from concurrent hooks
|
|
|
|
|
$lock_key = 'wbc_syncing_order_' . $order_id;
|
|
|
|
|
if ( get_transient( $lock_key ) ) {
|
|
|
|
|
WBC_Logger::debug( 'OrderSync', 'Order sync already in progress', array( 'order_id' => $order_id ) );
|
|
|
|
|
return array(
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => __( 'Order sync already in progress.', 'woo-business-central' ),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
set_transient( $lock_key, true, 120 );
|
|
|
|
|
|
|
|
|
|
WBC_Logger::info( 'OrderSync', 'Starting order sync', array( 'order_id' => $order_id ) );
|
|
|
|
|
|
|
|
|
|
// Step 1: Get or create customer in BC
|
|
|
|
|
$customer_number = $this->customer_sync->get_or_create_customer( $order );
|
|
|
|
|
|
|
|
|
|
if ( is_wp_error( $customer_number ) ) {
|
|
|
|
|
$this->add_order_note( $order, sprintf(
|
|
|
|
|
__( 'Failed to sync customer to Business Central: %s', 'woo-business-central' ),
|
|
|
|
|
$customer_number->get_error_message()
|
|
|
|
|
) );
|
|
|
|
|
return $customer_number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Create sales order in BC
|
|
|
|
|
$sales_order = $this->create_sales_order( $order, $customer_number );
|
|
|
|
|
|
|
|
|
|
if ( is_wp_error( $sales_order ) ) {
|
|
|
|
|
$this->add_order_note( $order, sprintf(
|
|
|
|
|
__( 'Failed to create sales order in Business Central: %s', 'woo-business-central' ),
|
|
|
|
|
$sales_order->get_error_message()
|
|
|
|
|
) );
|
|
|
|
|
return $sales_order;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$bc_order_id = $sales_order['id'];
|
|
|
|
|
$bc_order_number = $sales_order['number'] ?? '';
|
|
|
|
|
|
|
|
|
|
// Step 3: Add sales order lines
|
|
|
|
|
$lines_result = $this->create_sales_order_lines( $order, $bc_order_id );
|
|
|
|
|
|
|
|
|
|
if ( is_wp_error( $lines_result ) ) {
|
|
|
|
|
// Log the error but don't fail completely
|
|
|
|
|
WBC_Logger::warning( 'OrderSync', 'Some order lines failed to sync', array(
|
|
|
|
|
'order_id' => $order_id,
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'error' => $lines_result->get_error_message(),
|
|
|
|
|
) );
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 16:12:11 +01:00
|
|
|
// Step 3b: Release the sales order if enabled
|
|
|
|
|
if ( get_option( 'wbc_auto_release_order', 'no' ) === 'yes' ) {
|
|
|
|
|
$release_result = $this->release_sales_order( $bc_order_id );
|
|
|
|
|
|
|
|
|
|
if ( is_wp_error( $release_result ) ) {
|
|
|
|
|
WBC_Logger::warning( 'OrderSync', 'Failed to release sales order', array(
|
|
|
|
|
'order_id' => $order_id,
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'error' => $release_result->get_error_message(),
|
|
|
|
|
) );
|
|
|
|
|
$this->add_order_note( $order, sprintf(
|
|
|
|
|
__( 'Order created in BC but failed to release: %s', 'woo-business-central' ),
|
|
|
|
|
$release_result->get_error_message()
|
|
|
|
|
) );
|
|
|
|
|
} else {
|
|
|
|
|
WBC_Logger::info( 'OrderSync', 'Sales order released', array(
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 09:59:53 +01:00
|
|
|
// Step 4: Save BC order ID to WC order meta
|
|
|
|
|
$order->update_meta_data( '_wbc_bc_order_id', $bc_order_id );
|
|
|
|
|
$order->update_meta_data( '_wbc_bc_order_number', $bc_order_number );
|
|
|
|
|
$order->update_meta_data( '_wbc_synced_at', current_time( 'mysql' ) );
|
|
|
|
|
$order->save();
|
|
|
|
|
|
|
|
|
|
// Add order note
|
|
|
|
|
$this->add_order_note( $order, sprintf(
|
|
|
|
|
__( 'Order synced to Business Central. BC Order Number: %s', 'woo-business-central' ),
|
|
|
|
|
$bc_order_number
|
|
|
|
|
) );
|
|
|
|
|
|
|
|
|
|
// Release the sync lock
|
|
|
|
|
delete_transient( 'wbc_syncing_order_' . $order_id );
|
|
|
|
|
|
|
|
|
|
WBC_Logger::info( 'OrderSync', 'Order sync completed', array(
|
|
|
|
|
'order_id' => $order_id,
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'bc_order_number' => $bc_order_number,
|
|
|
|
|
) );
|
|
|
|
|
|
|
|
|
|
return array(
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => __( 'Order synced to Business Central.', 'woo-business-central' ),
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'bc_order_number' => $bc_order_number,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create sales order in BC
|
|
|
|
|
*
|
|
|
|
|
* @param WC_Order $order WooCommerce order.
|
|
|
|
|
* @param string $customer_number BC customer number.
|
|
|
|
|
* @return array|WP_Error Created sales order or error.
|
|
|
|
|
*/
|
|
|
|
|
private function create_sales_order( $order, $customer_number ) {
|
|
|
|
|
$order_data = array(
|
|
|
|
|
'customerNumber' => $customer_number,
|
|
|
|
|
'orderDate' => $order->get_date_created()->format( 'Y-m-d' ),
|
|
|
|
|
'externalDocumentNumber' => $order->get_order_number(),
|
|
|
|
|
'currencyCode' => $order->get_currency(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Add payment terms if configured
|
|
|
|
|
$payment_terms_id = get_option( 'wbc_default_payment_terms_id', '' );
|
|
|
|
|
if ( ! empty( $payment_terms_id ) ) {
|
|
|
|
|
$order_data['paymentTermsId'] = $payment_terms_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add shipment method if configured
|
|
|
|
|
$shipment_method_id = get_option( 'wbc_default_shipment_method_id', '' );
|
|
|
|
|
if ( ! empty( $shipment_method_id ) ) {
|
|
|
|
|
$order_data['shipmentMethodId'] = $shipment_method_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add shipping address if different from billing
|
|
|
|
|
$shipping_address_1 = $order->get_shipping_address_1();
|
|
|
|
|
if ( ! empty( $shipping_address_1 ) ) {
|
|
|
|
|
$order_data['shipToName'] = trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() );
|
|
|
|
|
$order_data['shipToAddressLine1'] = $shipping_address_1;
|
|
|
|
|
$order_data['shipToCity'] = $order->get_shipping_city();
|
|
|
|
|
$order_data['shipToState'] = $order->get_shipping_state();
|
|
|
|
|
$order_data['shipToPostCode'] = $order->get_shipping_postcode();
|
|
|
|
|
$order_data['shipToCountry'] = $order->get_shipping_country();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WBC_Logger::debug( 'OrderSync', 'Creating sales order in BC', array(
|
|
|
|
|
'order_id' => $order->get_id(),
|
|
|
|
|
'data' => $order_data,
|
|
|
|
|
) );
|
|
|
|
|
|
|
|
|
|
return WBC_API_Client::create_sales_order( $order_data );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create sales order lines in BC
|
|
|
|
|
*
|
|
|
|
|
* @param WC_Order $order WooCommerce order.
|
|
|
|
|
* @param string $bc_order_id BC sales order ID.
|
|
|
|
|
* @return true|WP_Error True on success or error.
|
|
|
|
|
*/
|
|
|
|
|
private function create_sales_order_lines( $order, $bc_order_id ) {
|
|
|
|
|
$errors = array();
|
|
|
|
|
$items = $order->get_items();
|
2026-02-17 18:48:36 +01:00
|
|
|
$location_code = get_option( 'wbc_location_code', '' );
|
2026-02-17 09:59:53 +01:00
|
|
|
|
|
|
|
|
foreach ( $items as $item_id => $item ) {
|
|
|
|
|
$product = $item->get_product();
|
|
|
|
|
|
|
|
|
|
if ( ! $product ) {
|
|
|
|
|
WBC_Logger::warning( 'OrderSync', 'Skipping order line - product not found', array(
|
|
|
|
|
'order_id' => $order->get_id(),
|
|
|
|
|
'item_id' => $item_id,
|
|
|
|
|
) );
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the BC item number (use SKU)
|
|
|
|
|
$bc_item_number = $product->get_sku();
|
|
|
|
|
|
|
|
|
|
if ( empty( $bc_item_number ) ) {
|
|
|
|
|
WBC_Logger::warning( 'OrderSync', 'Skipping order line - product has no SKU', array(
|
|
|
|
|
'order_id' => $order->get_id(),
|
|
|
|
|
'product_id' => $product->get_id(),
|
|
|
|
|
) );
|
|
|
|
|
$errors[] = sprintf( 'Product "%s" has no SKU', $product->get_name() );
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build line data
|
|
|
|
|
$line_data = array(
|
|
|
|
|
'lineType' => 'Item',
|
|
|
|
|
'lineObjectNumber' => $bc_item_number,
|
|
|
|
|
'quantity' => $item->get_quantity(),
|
|
|
|
|
'unitPrice' => (float) $order->get_item_total( $item, false, false ),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-17 18:48:36 +01:00
|
|
|
if ( ! empty( $location_code ) ) {
|
|
|
|
|
$line_data['locationCode'] = $location_code;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 09:59:53 +01:00
|
|
|
// Add description if different from product name
|
|
|
|
|
$line_description = $item->get_name();
|
|
|
|
|
if ( ! empty( $line_description ) ) {
|
|
|
|
|
$line_data['description'] = substr( $line_description, 0, 100 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WBC_Logger::debug( 'OrderSync', 'Creating sales order line', array(
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'data' => $line_data,
|
|
|
|
|
) );
|
|
|
|
|
|
|
|
|
|
$result = WBC_API_Client::create_sales_order_line( $bc_order_id, $line_data );
|
|
|
|
|
|
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
|
|
|
WBC_Logger::error( 'OrderSync', 'Failed to create sales order line', array(
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'bc_item_number' => $bc_item_number,
|
|
|
|
|
'error' => $result->get_error_message(),
|
|
|
|
|
) );
|
|
|
|
|
$errors[] = sprintf( 'Item %s: %s', $bc_item_number, $result->get_error_message() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle shipping as a line item if there's shipping cost
|
|
|
|
|
$shipping_total = (float) $order->get_shipping_total();
|
|
|
|
|
if ( $shipping_total > 0 ) {
|
|
|
|
|
$shipping_item_number = get_option( 'wbc_shipping_item_number', '' );
|
|
|
|
|
|
|
|
|
|
if ( ! empty( $shipping_item_number ) ) {
|
|
|
|
|
$shipping_line = array(
|
|
|
|
|
'lineType' => 'Item',
|
|
|
|
|
'lineObjectNumber' => $shipping_item_number,
|
|
|
|
|
'quantity' => 1,
|
|
|
|
|
'unitPrice' => $shipping_total,
|
|
|
|
|
'description' => __( 'Shipping', 'woo-business-central' ),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-17 18:48:36 +01:00
|
|
|
if ( ! empty( $location_code ) ) {
|
|
|
|
|
$shipping_line['locationCode'] = $location_code;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 09:59:53 +01:00
|
|
|
$result = WBC_API_Client::create_sales_order_line( $bc_order_id, $shipping_line );
|
|
|
|
|
|
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
|
|
|
WBC_Logger::warning( 'OrderSync', 'Failed to add shipping line', array(
|
|
|
|
|
'bc_order_id' => $bc_order_id,
|
|
|
|
|
'error' => $result->get_error_message(),
|
|
|
|
|
) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( ! empty( $errors ) ) {
|
|
|
|
|
return new WP_Error( 'wbc_line_errors', implode( '; ', $errors ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 16:12:11 +01:00
|
|
|
/**
|
|
|
|
|
* Release a sales order in BC (change status from Draft to Released)
|
|
|
|
|
*
|
|
|
|
|
* @param string $bc_order_id BC sales order ID.
|
|
|
|
|
* @return array|WP_Error Updated order or error.
|
|
|
|
|
*/
|
|
|
|
|
private function release_sales_order( $bc_order_id ) {
|
|
|
|
|
return WBC_API_Client::patch(
|
|
|
|
|
'/salesOrders(' . $bc_order_id . ')',
|
|
|
|
|
array( 'status' => 'Released' )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 09:59:53 +01:00
|
|
|
/**
|
|
|
|
|
* Add order note
|
|
|
|
|
*
|
|
|
|
|
* @param WC_Order $order WooCommerce order.
|
|
|
|
|
* @param string $message Note message.
|
|
|
|
|
*/
|
|
|
|
|
private function add_order_note( $order, $message ) {
|
|
|
|
|
$order->add_order_note( '[WBC] ' . $message );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Manual sync for an order (callable from admin)
|
|
|
|
|
*
|
|
|
|
|
* @param int $order_id WooCommerce order ID.
|
|
|
|
|
* @return array Sync result.
|
|
|
|
|
*/
|
|
|
|
|
public function manual_sync( $order_id ) {
|
|
|
|
|
$order = wc_get_order( $order_id );
|
|
|
|
|
|
|
|
|
|
if ( ! $order ) {
|
|
|
|
|
return array(
|
|
|
|
|
'success' => false,
|
|
|
|
|
'message' => __( 'Order not found.', 'woo-business-central' ),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear existing sync data to force re-sync
|
|
|
|
|
$order->delete_meta_data( '_wbc_bc_order_id' );
|
|
|
|
|
$order->delete_meta_data( '_wbc_bc_order_number' );
|
|
|
|
|
$order->save();
|
|
|
|
|
|
|
|
|
|
return $this->sync_order( $order_id );
|
|
|
|
|
}
|
|
|
|
|
}
|