class-api-for-cmbird.php
4 months ago
class-api-for-exact-webhooks.php
5 months ago
class-api-for-product-webhook.php
3 months ago
class-api-for-shipping-status.php
10 months ago
class-api-for-woo-order.php
3 months ago
class-api-for-zoho-inventory.php
11 months ago
class-commercebird-list-items-api-controller.php
11 months ago
class-commercebird-media-api-controller.php
11 months ago
class-commercebird-metadata-controller.php
3 months ago
index.php
1 year ago
trait-api-permission.php
2 months ago
class-api-for-woo-order.php
681 lines
| 1 | <?php |
| 2 | |
| 3 | namespace CommerceBird\API; |
| 4 | |
| 5 | if ( ! defined( 'ABSPATH' ) ) { |
| 6 | exit; |
| 7 | } |
| 8 | |
| 9 | use CommerceBird\Admin\Traits\LogWriter; |
| 10 | use WP_REST_Response; |
| 11 | |
| 12 | class CreateOrderWebhook { |
| 13 | |
| 14 | use Api; |
| 15 | use LogWriter; |
| 16 | |
| 17 | private static string $endpoint = 'create-woo-order'; |
| 18 | |
| 19 | |
| 20 | public function __construct() { |
| 21 | register_rest_route( |
| 22 | self::$namespace, |
| 23 | self::$endpoint, |
| 24 | array( |
| 25 | 'methods' => 'POST', |
| 26 | 'callback' => array( $this, 'handle' ), |
| 27 | 'permission_callback' => array( $this, 'permission_check' ), |
| 28 | ) |
| 29 | ); |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * @param $address |
| 34 | * |
| 35 | * @return array |
| 36 | */ |
| 37 | public function format_address( $address ): array { |
| 38 | if ( array_key_exists( 'attention', $address ) ) { |
| 39 | $names = explode( ' ', $address['attention'] ); |
| 40 | $first_name = $names[0] ?? ''; |
| 41 | $last_name = $names[1] ?? ''; |
| 42 | } |
| 43 | return array( |
| 44 | 'first_name' => $this->normalize_customer_text( $first_name ?? '' ), |
| 45 | 'last_name' => $this->normalize_customer_text( $last_name ?? '' ), |
| 46 | 'address_1' => $this->normalize_customer_text( $address['address'] ?? '' ), |
| 47 | 'address_2' => $this->normalize_customer_text( $address['street2'] ?? '' ), |
| 48 | 'city' => $this->normalize_customer_text( $address['city'] ?? '' ), |
| 49 | 'state' => $this->normalize_customer_text( $address['state_code'] ?? '' ), |
| 50 | 'postcode' => $this->normalize_customer_text( $address['zip'] ?? '' ), |
| 51 | 'country' => $this->normalize_customer_text( $address['country_code'] ?? '' ), |
| 52 | 'phone' => $this->normalize_customer_text( $address['phone'] ?? '' ), |
| 53 | ); |
| 54 | } |
| 55 | |
| 56 | private function normalize_customer_text( $value ): string { |
| 57 | if ( ! is_scalar( $value ) ) { |
| 58 | return ''; |
| 59 | } |
| 60 | |
| 61 | $normalized = remove_accents( (string) $value ); |
| 62 | $normalized = preg_replace( '/[^\x20-\x7E]/', '', $normalized ); |
| 63 | |
| 64 | if ( null === $normalized ) { |
| 65 | $normalized = ''; |
| 66 | } |
| 67 | |
| 68 | return trim( sanitize_text_field( $normalized ) ); |
| 69 | } |
| 70 | |
| 71 | private function process( array $order_data ): WP_REST_Response { |
| 72 | $response = new WP_REST_Response(); |
| 73 | |
| 74 | // Check if the order data is valid. |
| 75 | if ( empty( $order_data ) || empty( $order_data['salesorder'] ) ) { |
| 76 | $response->set_data( $this->empty_response ); |
| 77 | $response->set_status( 404 ); |
| 78 | return $response; |
| 79 | } |
| 80 | |
| 81 | // Validate salesorder_id exists. |
| 82 | if ( ! isset( $order_data['salesorder']['salesorder_id'] ) || empty( $order_data['salesorder']['salesorder_id'] ) ) { |
| 83 | /* translators: %1$s: Store name */ |
| 84 | $message = sprintf( __( 'Zoho order could not be created in your store %1$s because of missing order ID.', 'commercebird' ), get_bloginfo( 'name' ) ); |
| 85 | $common_class = new \CMBIRD_Common_Functions(); |
| 86 | $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message ); |
| 87 | $response->set_status( 500 ); |
| 88 | $response->set_data( $message ); |
| 89 | return $response; |
| 90 | } |
| 91 | |
| 92 | $allowed_keys = array( |
| 93 | 'salesorder_id', |
| 94 | 'salesorder_number', |
| 95 | 'customer_id', |
| 96 | 'customer_name', |
| 97 | 'billing_address', |
| 98 | 'shipping_address', |
| 99 | 'delivery_method', |
| 100 | 'currency_code', |
| 101 | 'line_items', |
| 102 | 'contact_person_details', |
| 103 | 'shipping_charge', |
| 104 | 'order_status', |
| 105 | 'paid_status', |
| 106 | 'invoiced_status', |
| 107 | 'total', |
| 108 | 'discount', |
| 109 | 'discount_total', |
| 110 | 'discount_percent', |
| 111 | 'notes', |
| 112 | 'salesperson_name', |
| 113 | 'reference_number', |
| 114 | ); |
| 115 | |
| 116 | $order_data = array_intersect_key( $order_data['salesorder'], array_flip( $allowed_keys ) ); |
| 117 | $billing_address = $this->format_address( $order_data['billing_address'] ); |
| 118 | $shipping_address = $this->format_address( $order_data['shipping_address'] ); |
| 119 | $billing_company = $this->normalize_customer_text( $order_data['customer_name'] ?? '' ); |
| 120 | $billing_address['company'] = $billing_company; |
| 121 | |
| 122 | $zoho_contact = array(); |
| 123 | |
| 124 | // Always fetch full contact details from Zoho API when customer_id is available,. |
| 125 | // since the webhook payload omits company_name, first_name, and last_name. |
| 126 | if ( ! empty( $order_data['customer_id'] ) ) { |
| 127 | $zoho_contact = $this->get_zoho_contact_details( $order_data['customer_id'] ); |
| 128 | if ( ! empty( $zoho_contact ) ) { |
| 129 | $order_data['contact_person_details'] = $zoho_contact; |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | // Handle customer creation/update. |
| 134 | $customer = null; |
| 135 | $contact_data = $zoho_contact[0] ?? ( $order_data['contact_person_details'][0] ?? null ); |
| 136 | if ( isset( $contact_data['email'] ) ) { |
| 137 | $customer_mail = $contact_data['email']; |
| 138 | $first_name = $this->normalize_customer_text( $contact_data['first_name'] ?? '' ); |
| 139 | $last_name = $this->normalize_customer_text( $contact_data['last_name'] ?? '' ); |
| 140 | $company_name = $this->normalize_customer_text( $contact_data['company_name'] ?? '' ); |
| 141 | $eu_vat_number = $contact_data['company_id'] ?? ''; |
| 142 | if ( '' === $company_name ) { |
| 143 | $company_name = $billing_company; |
| 144 | } |
| 145 | $customer = get_user_by( 'email', $customer_mail ); |
| 146 | if ( empty( $customer ) ) { |
| 147 | $customer_id = wc_create_new_customer( |
| 148 | $customer_mail, |
| 149 | '', |
| 150 | '', |
| 151 | array( |
| 152 | 'role' => 'customer', |
| 153 | 'first_name' => $first_name, |
| 154 | 'last_name' => $last_name, |
| 155 | ) |
| 156 | ); |
| 157 | $wc_customer = new \WC_Customer( $customer_id ); |
| 158 | // Set billing details. |
| 159 | $wc_customer->set_billing_first_name( $first_name ); |
| 160 | $wc_customer->set_billing_last_name( $last_name ); |
| 161 | $wc_customer->set_billing_company( $company_name ); |
| 162 | $wc_customer->set_billing_address_1( $billing_address['address_1'] ); |
| 163 | $wc_customer->set_billing_address_2( $billing_address['address_2'] ); |
| 164 | $wc_customer->set_billing_city( $billing_address['city'] ); |
| 165 | $wc_customer->set_billing_postcode( $billing_address['postcode'] ); |
| 166 | $wc_customer->set_billing_country( $billing_address['country'] ); |
| 167 | $wc_customer->set_billing_state( $billing_address['state'] ); |
| 168 | $wc_customer->set_billing_email( $customer_mail ); |
| 169 | $wc_customer->set_billing_phone( $billing_address['phone'] ); |
| 170 | // Set shipping details. |
| 171 | $wc_customer->set_shipping_first_name( $first_name ); |
| 172 | $wc_customer->set_shipping_last_name( $last_name ); |
| 173 | $wc_customer->set_shipping_address_1( $shipping_address['address_1'] ); |
| 174 | $wc_customer->set_shipping_address_2( $shipping_address['address_2'] ); |
| 175 | $wc_customer->set_shipping_city( $shipping_address['city'] ); |
| 176 | $wc_customer->set_shipping_postcode( $shipping_address['postcode'] ); |
| 177 | $wc_customer->set_shipping_country( $shipping_address['country'] ); |
| 178 | $wc_customer->set_shipping_state( $shipping_address['state'] ); |
| 179 | $wc_customer->save(); |
| 180 | $customer = get_user_by( 'id', $customer_id ); |
| 181 | } else { |
| 182 | $wc_customer = new \WC_Customer( $customer->ID ); |
| 183 | // Set billing details. |
| 184 | $wc_customer->set_billing_first_name( $first_name ); |
| 185 | $wc_customer->set_billing_last_name( $last_name ); |
| 186 | $wc_customer->set_billing_company( $company_name ); |
| 187 | $wc_customer->set_billing_address_1( $billing_address['address_1'] ); |
| 188 | $wc_customer->set_billing_address_2( $billing_address['address_2'] ); |
| 189 | $wc_customer->set_billing_city( $billing_address['city'] ); |
| 190 | $wc_customer->set_billing_postcode( $billing_address['postcode'] ); |
| 191 | $wc_customer->set_billing_country( $billing_address['country'] ); |
| 192 | $wc_customer->set_billing_state( $billing_address['state'] ); |
| 193 | $wc_customer->set_billing_email( $customer_mail ); |
| 194 | $wc_customer->set_billing_phone( $billing_address['phone'] ); |
| 195 | // Set shipping details. |
| 196 | $wc_customer->set_shipping_first_name( $first_name ); |
| 197 | $wc_customer->set_shipping_last_name( $last_name ); |
| 198 | $wc_customer->set_shipping_address_1( $shipping_address['address_1'] ); |
| 199 | $wc_customer->set_shipping_address_2( $shipping_address['address_2'] ); |
| 200 | $wc_customer->set_shipping_city( $shipping_address['city'] ); |
| 201 | $wc_customer->set_shipping_postcode( $shipping_address['postcode'] ); |
| 202 | $wc_customer->set_shipping_country( $shipping_address['country'] ); |
| 203 | $wc_customer->set_shipping_state( $shipping_address['state'] ); |
| 204 | $wc_customer->save(); |
| 205 | } |
| 206 | |
| 207 | if ( class_exists( 'WC_EU_VAT_Number' ) && '' !== $eu_vat_number && ! empty( $customer ) ) { |
| 208 | update_user_meta( $customer->ID, 'vat_number', $eu_vat_number ); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | // Validate line items exist. |
| 213 | if ( empty( $order_data['line_items'] ) ) { |
| 214 | // translators: 1: order number, 2: Store name. |
| 215 | $message = sprintf( __( 'Zoho order #%1$s could not be created in your store %2$s because of missing line items.', 'commercebird' ), $order_data['salesorder_number'], get_bloginfo( 'name' ) ); |
| 216 | $common_class = new \CMBIRD_Common_Functions(); |
| 217 | $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message ); |
| 218 | $response->set_status( 500 ); |
| 219 | $response->set_data( $message ); |
| 220 | return $response; |
| 221 | } |
| 222 | |
| 223 | $line_items_result = $this->get_items( $order_data['line_items'] ); |
| 224 | $line_items = $line_items_result['items']; |
| 225 | $shipping_charge = ! empty( $line_items_result['shipping_total'] ) |
| 226 | ? (float) $line_items_result['shipping_total'] |
| 227 | : ( isset( $order_data['shipping_charge'] ) ? (float) $order_data['shipping_charge'] : 0.0 ); |
| 228 | |
| 229 | if ( ! empty( $line_items_result['not_found'] ) ) { |
| 230 | // translators: 1: order number, 2: Store name, 3: missing items. |
| 231 | $message = sprintf( __( 'Zoho order #%1$s could not be created in your store %2$s because of missing items: %3$s', 'commercebird' ), $order_data['salesorder_number'], get_bloginfo( 'name' ), implode( ', ', $line_items_result['not_found'] ) ); |
| 232 | $common_class = new \CMBIRD_Common_Functions(); |
| 233 | $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message ); |
| 234 | $response->set_status( 500 ); |
| 235 | $response->set_data( $message ); |
| 236 | return $response; |
| 237 | } |
| 238 | |
| 239 | $order_discount_total = isset( $order_data['discount_total'] ) ? (float) $order_data['discount_total'] : 0.0; |
| 240 | $sales_order_total = isset( $order_data['total'] ) ? (float) $order_data['total'] : null; |
| 241 | |
| 242 | // Check if order already exists. |
| 243 | $orders = wc_get_orders( |
| 244 | array( |
| 245 | 'limit' => 1, |
| 246 | 'meta_key' => 'zi_salesorder_id', |
| 247 | 'meta_value' => $order_data['salesorder_id'], |
| 248 | 'meta_compare' => '=', |
| 249 | 'return' => 'ids', |
| 250 | ) |
| 251 | ); |
| 252 | if ( ! empty( $orders ) ) { |
| 253 | $existing_order = wc_get_order( $orders[0] ); |
| 254 | } |
| 255 | |
| 256 | // Update existing order. |
| 257 | if ( ! empty( $existing_order ) && empty( $line_items_result['not_found'] ) ) { |
| 258 | // if order status is completed or processing, do not update the order and return error response. |
| 259 | if ( in_array( $existing_order->get_status(), array( 'completed', 'processing' ), true ) ) { |
| 260 | /* translators: %1$s: Zoho order number, %2$s: Store name, %3$s: Order status */ |
| 261 | $message = sprintf( __( 'Zoho order #%1$s could not be updated in your store %2$s because the order is already in %3$s status.', 'commercebird' ), $order_data['salesorder_number'], get_bloginfo( 'name' ), $existing_order->get_status() ); |
| 262 | $common_class = new \CMBIRD_Common_Functions(); |
| 263 | $common_class->send_email( __( 'Zoho Order Sync', 'commercebird' ), $message ); |
| 264 | $response->set_status( 200 ); |
| 265 | $response->set_data( $message ); |
| 266 | return $response; |
| 267 | } |
| 268 | if ( $customer ) { |
| 269 | $wc_customer_data = ( new \WC_Customer( $customer->ID ) )->get_data(); |
| 270 | $existing_order->set_address( $wc_customer_data['billing'], 'billing' ); |
| 271 | $existing_order->set_address( $wc_customer_data['shipping'], 'shipping' ); |
| 272 | } else { |
| 273 | $existing_order->set_address( $billing_address, 'billing' ); |
| 274 | $existing_order->set_address( $shipping_address, 'shipping' ); |
| 275 | } |
| 276 | $existing_order->set_billing_company( $billing_company ); |
| 277 | // Remove existing order items. |
| 278 | $existing_order->remove_order_items( 'line_item' ); |
| 279 | // Add products to the order. |
| 280 | foreach ( $line_items as $line_item ) { |
| 281 | $product_id = $line_item['id']; |
| 282 | $quantity = $line_item['quantity']; |
| 283 | $rate = $line_item['rate'] ?? null; |
| 284 | if ( ! empty( $product_id ) ) { |
| 285 | $product = wc_get_product( $product_id ); |
| 286 | if ( $product && $product->is_type( 'variation' ) ) { |
| 287 | // Get variation object. |
| 288 | $variation = wc_get_product_object( 'variation', $product_id ); |
| 289 | if ( $variation ) { |
| 290 | $item_id = $existing_order->add_product( $variation, $quantity ); |
| 291 | } |
| 292 | } else { |
| 293 | $item_id = $existing_order->add_product( $product, $quantity ); |
| 294 | } |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | // Set order details. |
| 299 | $existing_order->set_customer_note( isset( $order_data['notes'] ) ? $order_data['notes'] : '' ); |
| 300 | $existing_order->update_status( $this->map_status( $order_data['paid_status'], $order_data['invoiced_status'] ?? '' ) ); |
| 301 | |
| 302 | // Handle shipping. |
| 303 | // Remove existing shipping items first. |
| 304 | foreach ( $existing_order->get_items( 'shipping' ) as $item_id => $item ) { |
| 305 | $existing_order->remove_item( $item_id ); |
| 306 | } |
| 307 | |
| 308 | if ( $shipping_charge > 0 ) { |
| 309 | $shipping = new \WC_Order_Item_Shipping(); |
| 310 | $shipping->set_method_title( $order_data['delivery_method'] ?? 'Flat Rate' ); |
| 311 | $shipping->set_method_id( 'flat_rate' ); |
| 312 | $shipping->set_total( $shipping_charge ); |
| 313 | $existing_order->add_item( $shipping ); |
| 314 | } |
| 315 | // Set order totals and save. |
| 316 | $existing_order->calculate_totals(); |
| 317 | $this->apply_order_discount_total( $existing_order, $order_discount_total, $sales_order_total ); |
| 318 | $existing_order->save(); |
| 319 | $note = 'Updated in Zoho Inventory'; |
| 320 | if ( ! empty( $order_data['salesperson_name'] ) ) { |
| 321 | $note .= ' by ' . $order_data['salesperson_name']; |
| 322 | } |
| 323 | $existing_order->add_order_note( $note ); |
| 324 | |
| 325 | $response->set_data( |
| 326 | array( |
| 327 | 'order_id' => $existing_order->get_id(), |
| 328 | 'total' => $existing_order->get_total(), |
| 329 | 'status' => $existing_order->get_status(), |
| 330 | ) |
| 331 | ); |
| 332 | $response->set_status( 200 ); |
| 333 | } elseif ( empty( $line_items_result['not_found'] ) ) { |
| 334 | // Create new order. |
| 335 | $order = wc_create_order(); |
| 336 | if ( $customer ) { |
| 337 | $order->set_customer_id( $customer->ID ); |
| 338 | $wc_customer_data = ( new \WC_Customer( $customer->ID ) )->get_data(); |
| 339 | $order->set_address( $wc_customer_data['billing'], 'billing' ); |
| 340 | $order->set_address( $wc_customer_data['shipping'], 'shipping' ); |
| 341 | } |
| 342 | $order->set_billing_company( $billing_company ); |
| 343 | // Add products to the order. |
| 344 | foreach ( $line_items as $line_item ) { |
| 345 | $product_id = $line_item['id']; |
| 346 | $quantity = $line_item['quantity']; |
| 347 | $rate = $line_item['rate'] ?? null; |
| 348 | if ( ! empty( $product_id ) ) { |
| 349 | $product = wc_get_product( $product_id ); |
| 350 | if ( $product && $product->is_type( 'variation' ) ) { |
| 351 | // Get variation object. |
| 352 | $variation = wc_get_product_object( 'variation', $product_id ); |
| 353 | if ( $variation ) { |
| 354 | $item_id = $order->add_product( $variation, $quantity ); |
| 355 | } |
| 356 | } else { |
| 357 | $item_id = $order->add_product( $product, $quantity ); |
| 358 | } |
| 359 | } |
| 360 | } |
| 361 | $order->set_currency( $order_data['currency_code'] ); |
| 362 | $order->set_status( $this->map_status( $order_data['paid_status'], $order_data['invoiced_status'] ?? '' ) ); |
| 363 | // Handle shipping. |
| 364 | if ( $shipping_charge > 0 ) { |
| 365 | $shipping = new \WC_Order_Item_Shipping(); |
| 366 | $shipping->set_method_title( $order_data['delivery_method'] ?? 'Flat Rate' ); |
| 367 | $shipping->set_method_id( 'flat_rate' ); |
| 368 | $shipping->set_total( $shipping_charge ); |
| 369 | $order->add_item( $shipping ); |
| 370 | } |
| 371 | |
| 372 | $order->calculate_totals(); |
| 373 | $this->apply_order_discount_total( $order, $order_discount_total, $sales_order_total ); |
| 374 | $order->set_customer_note( $order_data['notes'] ?? '' ); |
| 375 | $order->save(); |
| 376 | $order->update_meta_data( 'zi_salesorder_id', $order_data['salesorder_id'] ); |
| 377 | $order->save(); |
| 378 | $note = 'Synced in Zoho Inventory'; |
| 379 | if ( ! empty( $order_data['salesperson_name'] ) ) { |
| 380 | $note .= ' by ' . $order_data['salesperson_name']; |
| 381 | } |
| 382 | $order->add_order_note( $note ); |
| 383 | |
| 384 | // Update Zoho sales order with WooCommerce order ID as reference number. |
| 385 | if ( empty( $order_data['reference_number'] ) ) { |
| 386 | $this->update_zoho_reference_number( $order, $order_data['salesorder_id'] ); |
| 387 | } |
| 388 | $response->set_data( |
| 389 | array( |
| 390 | 'order_id' => $order->get_id(), |
| 391 | 'status' => $order->get_status(), |
| 392 | ) |
| 393 | ); |
| 394 | $response->set_status( 200 ); |
| 395 | } else { |
| 396 | $response->set_data( |
| 397 | array( |
| 398 | 'status' => 'Something went wrong. Please try again.', |
| 399 | ) |
| 400 | ); |
| 401 | $response->set_status( 500 ); |
| 402 | } |
| 403 | |
| 404 | return $response; |
| 405 | } |
| 406 | |
| 407 | /** |
| 408 | * Apply external discount amount from Zoho to WooCommerce order totals. |
| 409 | * |
| 410 | * @param \WC_Order $order WooCommerce order object. |
| 411 | * @param float $discount_total Discount total from payload. |
| 412 | * @param float|null $sales_order_total Zoho sales order total from payload. |
| 413 | * @return void |
| 414 | */ |
| 415 | private function apply_order_discount_total( \WC_Order $order, float $discount_total, ?float $sales_order_total = null ): void { |
| 416 | $current_total = (float) $order->get_total(); |
| 417 | |
| 418 | // When the Zoho total is provided, the gap between the calculated WC total and the |
| 419 | // Zoho total is the authoritative discount — using discount_total on top would double-count. |
| 420 | if ( null !== $sales_order_total && $sales_order_total >= 0 && $sales_order_total < $current_total ) { |
| 421 | $effective_discount = $current_total - $sales_order_total; |
| 422 | } else { |
| 423 | $effective_discount = max( 0, $discount_total ); |
| 424 | } |
| 425 | |
| 426 | if ( $effective_discount <= 0 ) { |
| 427 | $order->set_discount_total( 0 ); |
| 428 | $order->set_discount_tax( 0 ); |
| 429 | return; |
| 430 | } |
| 431 | |
| 432 | $effective_discount = min( $effective_discount, $current_total ); |
| 433 | $order->set_discount_total( $effective_discount ); |
| 434 | $order->set_discount_tax( 0 ); |
| 435 | $order->set_total( max( 0, $current_total - $effective_discount ) ); |
| 436 | } |
| 437 | |
| 438 | |
| 439 | private function map_status( $paid_status, $invoiced_status = '' ): string { |
| 440 | // If invoiced, status must be processing. |
| 441 | if ( 'invoiced' === $invoiced_status ) { |
| 442 | return 'wc-processing'; |
| 443 | } |
| 444 | |
| 445 | // Otherwise check paid status. |
| 446 | switch ( $paid_status ) { |
| 447 | case 'paid': |
| 448 | return 'wc-completed'; |
| 449 | default: |
| 450 | return 'wc-pending'; |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | |
| 455 | /** |
| 456 | * Update Zoho sales order reference number with WooCommerce order ID. |
| 457 | * |
| 458 | * @param \WC_Order $order WooCommerce order object. |
| 459 | * @param string $salesorder_id Zoho sales order ID. |
| 460 | * @return void |
| 461 | */ |
| 462 | private function update_zoho_reference_number( $order, $salesorder_id ): void { |
| 463 | // Get Zoho Inventory configuration. |
| 464 | $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' ); |
| 465 | $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' ); |
| 466 | |
| 467 | if ( empty( $zoho_inventory_url ) || empty( $zoho_inventory_oid ) || empty( $salesorder_id ) ) { |
| 468 | return; |
| 469 | } |
| 470 | |
| 471 | // Return if the order status is processing or completed, since the reference number should not be updated in that case. |
| 472 | if ( in_array( $order->get_status(), array( 'processing', 'completed' ), true ) ) { |
| 473 | return; |
| 474 | } |
| 475 | |
| 476 | // Prepare reference number using the same logic as order sync. |
| 477 | $transaction_id = $order->get_transaction_id(); |
| 478 | if ( empty( $transaction_id ) ) { |
| 479 | $transaction_id = $order->get_meta( '_order_number', true ); |
| 480 | } |
| 481 | |
| 482 | $order_prefix = get_option( 'cmbird_zoho_order_prefix_status' ); |
| 483 | $reference_number = ''; |
| 484 | |
| 485 | if ( class_exists( 'WCJ_Order_Numbers' ) || class_exists( 'WC_Seq_Order_Number_Pro' ) ) { |
| 486 | $reference_number = $order_prefix . $transaction_id; |
| 487 | } elseif ( ! empty( $order_prefix ) ) { |
| 488 | $reference_number = $order_prefix . '-' . $order->get_id(); |
| 489 | } else { |
| 490 | $reference_number = $order->get_id(); |
| 491 | } |
| 492 | |
| 493 | // Build the API URL. |
| 494 | $url = $zoho_inventory_url . 'inventory/v1/salesorders/' . $salesorder_id . '?organization_id=' . $zoho_inventory_oid; |
| 495 | |
| 496 | // Prepare data for update. |
| 497 | $data = array( |
| 498 | 'JSONString' => wp_json_encode( |
| 499 | array( |
| 500 | 'reference_number' => $reference_number, |
| 501 | ) |
| 502 | ), |
| 503 | ); |
| 504 | |
| 505 | // Make the API call to update Zoho sales order. |
| 506 | $api_handler = new \CMBIRD_API_Handler_Zoho(); |
| 507 | $response = $api_handler->execute_curl_call_put( $url, $data ); |
| 508 | |
| 509 | // Log any errors. |
| 510 | $logger = wc_get_logger(); |
| 511 | if ( is_wp_error( $response ) ) { |
| 512 | $logger->error( |
| 513 | 'Error updating Zoho reference number: ' . $response->get_error_message(), |
| 514 | array( |
| 515 | 'source' => 'commercebird', |
| 516 | 'order_id' => $order->get_id(), |
| 517 | 'salesorder_id' => $salesorder_id, |
| 518 | ) |
| 519 | ); |
| 520 | } elseif ( isset( $response->code ) && 0 !== $response->code ) { |
| 521 | $error_message = isset( $response->message ) ? $response->message : 'Unknown error'; |
| 522 | $logger->error( |
| 523 | 'Error updating Zoho reference number: ' . $error_message, |
| 524 | array( |
| 525 | 'source' => 'commercebird', |
| 526 | 'order_id' => $order->get_id(), |
| 527 | 'salesorder_id' => $salesorder_id, |
| 528 | ) |
| 529 | ); |
| 530 | } else { |
| 531 | $order->add_order_note( 'Updated Zoho reference number to: ' . $reference_number ); |
| 532 | } |
| 533 | } |
| 534 | |
| 535 | |
| 536 | /** |
| 537 | * Find the product ids from the line items from Zoho Inventory. |
| 538 | * |
| 539 | * @param mixed $line_items Line items from the salesorder. |
| 540 | * @return array |
| 541 | */ |
| 542 | private function get_items( $line_items ): array { |
| 543 | $product_ids = array(); |
| 544 | $not_found = array(); |
| 545 | $shipping_total = 0.0; |
| 546 | |
| 547 | foreach ( $line_items as $item ) { |
| 548 | $line_item_name = isset( $item['name'] ) ? trim( (string) $item['name'] ) : trim( (string) ( $item['item_name'] ?? '' ) ); |
| 549 | if ( 'shipping' === strtolower( $line_item_name ) ) { |
| 550 | $line_item_total = isset( $item['item_total'] ) ? (float) $item['item_total'] : 0.0; |
| 551 | if ( $line_item_total <= 0 && isset( $item['rate'] ) ) { |
| 552 | $quantity = isset( $item['quantity'] ) ? (float) $item['quantity'] : 1; |
| 553 | $line_item_total = (float) $item['rate'] * $quantity; |
| 554 | } |
| 555 | $shipping_total += $line_item_total; |
| 556 | continue; |
| 557 | } |
| 558 | |
| 559 | // Check if the sku key is present (case-insensitive check). |
| 560 | $sku = isset( $item['sku'] ) ? $item['sku'] : ( isset( $item['SKU'] ) ? $item['SKU'] : null ); |
| 561 | $quantity = $item['quantity'] ?? 1; |
| 562 | $rate = $item['rate'] ?? null; |
| 563 | |
| 564 | if ( null !== $sku ) { |
| 565 | $product_id = wc_get_product_id_by_sku( $sku ); |
| 566 | if ( $product_id ) { |
| 567 | // Append to product_ids instead of overwriting. |
| 568 | $product_ids[] = array( |
| 569 | 'id' => $product_id, |
| 570 | 'quantity' => $quantity, |
| 571 | 'rate' => $rate, |
| 572 | ); |
| 573 | } else { |
| 574 | // create the item in WooCommerce. |
| 575 | try { |
| 576 | $new_product = new \WC_Product_Simple(); |
| 577 | $new_product->set_name( $line_item_name ); |
| 578 | $new_product->set_sku( $sku ); |
| 579 | $new_product->set_regular_price( $rate ); |
| 580 | $new_product->set_status( 'publish' ); |
| 581 | $new_product->set_manage_stock( false ); |
| 582 | // add meta zi_item_id to link the product with Zoho item. |
| 583 | $new_product->add_meta_data( 'zi_item_id', $item['item_id'] ?? '' ); |
| 584 | $product_id = $new_product->save(); |
| 585 | if ( $product_id > 0 ) { |
| 586 | $product_ids[] = array( |
| 587 | 'id' => $product_id, |
| 588 | 'quantity' => $quantity, |
| 589 | 'rate' => $rate, |
| 590 | ); |
| 591 | } else { |
| 592 | $error_msg = 'Unknown error'; |
| 593 | $logger = wc_get_logger(); |
| 594 | $logger->error( |
| 595 | 'Failed to create product with SKU ' . $sku . ': ' . $error_msg, |
| 596 | array( 'source' => 'commercebird' ) |
| 597 | ); |
| 598 | $not_found[] = $line_item_name . ' (SKU: ' . $sku . ')'; |
| 599 | } |
| 600 | } catch ( \Throwable $e ) { |
| 601 | $logger = wc_get_logger(); |
| 602 | $logger->error( |
| 603 | 'Exception creating product with SKU ' . $sku . ': ' . $e->getMessage(), |
| 604 | array( 'source' => 'commercebird' ) |
| 605 | ); |
| 606 | $not_found[] = $line_item_name . ' (SKU: ' . $sku . ')'; |
| 607 | } |
| 608 | } |
| 609 | } else { |
| 610 | $not_found[] = 'Unknown SKU'; |
| 611 | } |
| 612 | } |
| 613 | |
| 614 | return array( |
| 615 | 'items' => $product_ids, |
| 616 | 'not_found' => $not_found, |
| 617 | 'shipping_total' => $shipping_total, |
| 618 | ); |
| 619 | } |
| 620 | |
| 621 | /** |
| 622 | * Fetch contact person details from Zoho Inventory using a customer_id. |
| 623 | * Returns an array compatible with contact_person_details format. |
| 624 | * |
| 625 | * @param string $customer_id Zoho customer/contact ID. |
| 626 | * @return array |
| 627 | */ |
| 628 | private function get_zoho_contact_details( string $customer_id ): array { |
| 629 | $zoho_inventory_url = get_option( 'cmbird_zoho_inventory_url' ); |
| 630 | $zoho_inventory_oid = get_option( 'cmbird_zoho_inventory_oid' ); |
| 631 | |
| 632 | if ( empty( $zoho_inventory_url ) || empty( $zoho_inventory_oid ) ) { |
| 633 | return array(); |
| 634 | } |
| 635 | |
| 636 | $url = $zoho_inventory_url . 'inventory/v1/contacts/' . $customer_id . '?organization_id=' . $zoho_inventory_oid; |
| 637 | |
| 638 | $api_handler = new \CMBIRD_API_Handler_Zoho(); |
| 639 | $response = $api_handler->execute_curl_call_get( $url ); |
| 640 | |
| 641 | if ( is_wp_error( $response ) || empty( $response->contact ) ) { |
| 642 | return array(); |
| 643 | } |
| 644 | |
| 645 | $contact = (array) $response->contact; |
| 646 | $contact_persons = array(); |
| 647 | |
| 648 | $company_name = $contact['company_name'] ?? ''; |
| 649 | $company_id = $contact['company_id'] ?? ''; |
| 650 | |
| 651 | if ( ! empty( $contact['contact_persons'] ) ) { |
| 652 | foreach ( $contact['contact_persons'] as $person ) { |
| 653 | $person = (array) $person; |
| 654 | if ( ! empty( $person['is_primary_contact'] ) && ! empty( $person['email'] ) ) { |
| 655 | $contact_persons[] = array( |
| 656 | 'email' => $person['email'], |
| 657 | 'first_name' => $person['first_name'] ?? '', |
| 658 | 'last_name' => $person['last_name'] ?? '', |
| 659 | 'company_name' => $company_name, |
| 660 | 'company_id' => $company_id, |
| 661 | ); |
| 662 | break; |
| 663 | } |
| 664 | } |
| 665 | } |
| 666 | |
| 667 | // Fall back to the contact's own email if no contact persons with email found. |
| 668 | if ( empty( $contact_persons ) && ! empty( $contact['email'] ) ) { |
| 669 | $contact_persons[] = array( |
| 670 | 'email' => $contact['email'], |
| 671 | 'first_name' => $contact['first_name'] ?? '', |
| 672 | 'last_name' => $contact['last_name'] ?? '', |
| 673 | 'company_name' => $company_name, |
| 674 | 'company_id' => $company_id, |
| 675 | ); |
| 676 | } |
| 677 | |
| 678 | return $contact_persons; |
| 679 | } |
| 680 | } |
| 681 |