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(); $location_code = get_option( 'wbc_location_code', '' ); 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 ), ); if ( ! empty( $location_code ) ) { $line_data['locationCode'] = $location_code; } // 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' ), ); if ( ! empty( $location_code ) ) { $shipping_line['locationCode'] = $location_code; } $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 ); } }