Files
WooBC/woo-business-central/includes/class-wbc-order-sync.php
Malin 5716ff7742 feat: add auto-release sales order option
PATCH salesOrders status to 'Released' after creation and line items.
Controlled by Auto-Release Order checkbox in Order Settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:12:11 +01:00

368 lines
13 KiB
PHP

<?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(),
) );
}
// 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,
) );
}
}
// 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();
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 ),
);
// 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' ),
);
$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;
}
/**
* 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' )
);
}
/**
* 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 );
}
}