get_id() ); } } public static function is_cod( WC_Order $order ) { $cod = (array) MarkitMall_DS_Settings::get_all()['cod_gateways']; return in_array( $order->get_payment_method(), $cod, true ); } /** Orders go to MarkitMall without anyone clicking, unless turned off in the settings. */ public static function auto_send() { return 'no' !== MarkitMall_DS_Settings::get_all()['auto_send']; } /** Paid, or cash on delivery: MarkitMall can take it. */ public static function is_sendable( WC_Order $order ) { return self::is_cod( $order ) || $order->is_paid(); } /** Send when it is ready: COD at once, prepaid once paid; never twice. */ public static function maybe_send( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || ! MarkitMall_DS_API::is_configured() ) { return; } if ( ! self::auto_send() && ! in_array( $order->get_meta( MARKITMALL_DS_META_STATE ), array( 'sending', 'retrying' ), true ) ) { return; } $state = $order->get_meta( MARKITMALL_DS_META_STATE ); // 'sending' is not skipped: if PHP died mid-request the order would be // stuck there forever, and the Idempotency-Key makes a resend safe. if ( in_array( $state, array( 'sent', 'none' ), true ) ) { return; } if ( ! self::is_cod( $order ) && ! $order->is_paid() ) { return; } self::send_order( $order_id ); } public static function retry( $order_id ) { self::send_order( $order_id ); } /** Build the MarkitMall order from the WooCommerce order. */ private static function build_payload( WC_Order $order ) { $items = array(); foreach ( $order->get_items() as $item_id => $item ) { /** @var WC_Order_Item_Product $item */ $product = $item->get_product(); if ( ! $product ) { continue; } $variant_id = $product->get_meta( MARKITMALL_DS_META_VARIANT ); if ( ! $variant_id ) { continue; // The reseller's own product — not ours to ship. } $qty = max( 1, (int) $item->get_quantity() ); $items[] = array( 'variantId' => $variant_id, 'quantity' => $qty, // What the customer actually paid per unit, after discounts. 'resalePrice' => (int) round( ( (float) $item->get_total() + (float) $item->get_total_tax() ) / $qty ), 'externalLineId' => (string) $item_id, 'sku' => $product->get_sku() ? $product->get_sku() : null, ); } if ( ! $items ) { return null; } $name = trim( $order->get_formatted_shipping_full_name() ); $address = trim( $order->get_shipping_address_1() . ' ' . $order->get_shipping_address_2() ); $city = $order->get_shipping_city(); $state = $order->get_shipping_state(); if ( '' === $address ) { $name = trim( $order->get_formatted_billing_full_name() ); $address = trim( $order->get_billing_address_1() . ' ' . $order->get_billing_address_2() ); $city = $order->get_billing_city(); $state = $order->get_billing_state(); } $phone = method_exists( $order, 'get_shipping_phone' ) && $order->get_shipping_phone() ? $order->get_shipping_phone() : $order->get_billing_phone(); $states = WC()->countries ? WC()->countries->get_states( 'PK' ) : array(); $sender = MarkitMall_DS_Settings::get_all()['sender_name']; $payload = array( 'externalOrderId' => (string) $order->get_id(), 'externalOrderName' => '#' . $order->get_order_number(), 'paymentMode' => self::is_cod( $order ) ? 'COD' : 'PREPAID', 'customer' => array_filter( array( 'name' => $name ? $name : __( 'Customer', 'markitmall-dropship' ), 'phone' => $phone, 'address' => $address, 'city' => $city, 'province' => isset( $states[ $state ] ) ? $states[ $state ] : $state, 'postalCode' => $order->get_shipping_postcode() ? $order->get_shipping_postcode() : null, ) ), 'items' => $items, 'shippingCharged' => (int) round( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax() ), 'notes' => $order->get_customer_note() ? mb_substr( $order->get_customer_note(), 0, 500 ) : null, ); if ( $sender ) { $payload['senderName'] = $sender; } return array_filter( $payload, static function ( $v ) { return null !== $v; } ); } /** * Send one order. With $manual the attempt counter is reset (admin "Retry"). */ public static function send_order( $order_id, $manual = false ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } if ( 'sent' === $order->get_meta( MARKITMALL_DS_META_STATE ) ) { return; } $payload = self::build_payload( $order ); if ( null === $payload ) { $order->update_meta_data( MARKITMALL_DS_META_STATE, 'none' ); $order->save(); return; } if ( 'PKR' !== $order->get_currency() ) { $message = __( 'MarkitMall delivers in Pakistan and settles in PKR; this order is in another currency.', 'markitmall-dropship' ); $order->update_meta_data( MARKITMALL_DS_META_STATE, 'failed' ); $order->update_meta_data( MARKITMALL_DS_META_ERROR, $message ); $order->add_order_note( sprintf( /* translators: %s: error */ __( 'MarkitMall did not accept this order: %s', 'markitmall-dropship' ), $message ) ); $order->save(); return; } $order->update_meta_data( MARKITMALL_DS_META_STATE, 'sending' ); $attempts = $manual ? 0 : (int) $order->get_meta( MARKITMALL_DS_META_ATTEMPTS ); $order->update_meta_data( MARKITMALL_DS_META_ATTEMPTS, $attempts + 1 ); $order->save(); $host = wp_parse_url( home_url(), PHP_URL_HOST ); $result = MarkitMall_DS_API::post( '/orders', $payload, array( 'Idempotency-Key' => 'woo-' . $host . '-' . $order->get_id() ) ); if ( is_wp_error( $result ) && 'duplicate_order' === $result->get_error_code() ) { // Already there (e.g. sent before a crash): fetch it instead. $existing = MarkitMall_DS_API::get( '/orders?externalOrderId=' . rawurlencode( (string) $order->get_id() ) ); if ( ! is_wp_error( $existing ) && ! empty( $existing['data'] ) ) { $result = array( 'parcels' => $existing['data'], 'status' => $existing['data'][0]['status'] ); } } if ( is_wp_error( $result ) ) { $code = $result->get_error_code(); $message = $result->get_error_message(); $retry = in_array( $code, self::RETRYABLE, true ) || 0 === strpos( $code, 'http_5' ); if ( $retry && $attempts + 1 < self::MAX_RETRIES && function_exists( 'as_schedule_single_action' ) ) { $data = $result->get_error_data(); $delay = isset( $data['retry_after'] ) && $data['retry_after'] > 0 ? (int) $data['retry_after'] : min( 6 * HOUR_IN_SECONDS, 60 * pow( 3, $attempts ) ); as_schedule_single_action( time() + $delay, self::RETRY_HOOK, array( $order->get_id() ), 'markitmall-dropship' ); $order->update_meta_data( MARKITMALL_DS_META_STATE, 'retrying' ); $order->add_order_note( sprintf( /* translators: 1: error, 2: minutes */ __( 'MarkitMall: could not send the order (%1$s). Retrying in %2$d min.', 'markitmall-dropship' ), $message, (int) ceil( $delay / 60 ) ) ); } else { $order->update_meta_data( MARKITMALL_DS_META_STATE, 'failed' ); $order->add_order_note( sprintf( /* translators: %s: error */ __( 'MarkitMall did not accept this order: %s', 'markitmall-dropship' ), $message ) ); } $order->update_meta_data( MARKITMALL_DS_META_ERROR, mb_substr( $message, 0, 300 ) ); $order->save(); return; } $parcels = isset( $result['parcels'] ) ? $result['parcels'] : array(); $stored = array(); foreach ( $parcels as $parcel ) { $stored[] = array( 'id' => sanitize_text_field( $parcel['id'] ), 'orderNumber' => isset( $parcel['orderNumber'] ) ? sanitize_text_field( (string) $parcel['orderNumber'] ) : '', 'status' => sanitize_text_field( $parcel['status'] ), ); } $order->update_meta_data( MARKITMALL_DS_META_ORDERS, $stored ); $order->update_meta_data( MARKITMALL_DS_META_STATE, 'sent' ); $order->delete_meta_data( MARKITMALL_DS_META_ERROR ); $status = isset( $result['status'] ) ? $result['status'] : ''; if ( 'AWAITING_FUNDS' === $status ) { $order->add_order_note( __( 'MarkitMall: your wallet could not cover this prepaid order. Top up and press “Pay” on the order in your dropshipping dashboard, or it will be cancelled.', 'markitmall-dropship' ) ); } else { $numbers = implode( ', ', array_filter( wp_list_pluck( $stored, 'orderNumber' ) ) ); $note = sprintf( /* translators: %s: MarkitMall order numbers */ __( 'Sent to MarkitMall (%s).', 'markitmall-dropship' ), $numbers ); if ( isset( $result['codAmount'], $result['expectedMargin'] ) && 'COD' === $payload['paymentMode'] ) { $note .= ' ' . sprintf( /* translators: 1: amount, 2: margin */ __( 'Courier collects Rs %1$s; your margin after delivery: Rs %2$s.', 'markitmall-dropship' ), number_format_i18n( $result['codAmount'] ), number_format_i18n( $result['expectedMargin'] ) ); } if ( count( $stored ) > 1 ) { $note .= ' ' . __( 'The items ship as separate parcels.', 'markitmall-dropship' ); } $order->add_order_note( $note ); } $order->save(); } /** You cancelled the WooCommerce order: cancel it at MarkitMall too, if still possible. */ public static function on_cancelled( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order || 'sent' !== $order->get_meta( MARKITMALL_DS_META_STATE ) || $order->get_meta( '_markitmall_cancelled_by_mm' ) ) { return; } foreach ( (array) $order->get_meta( MARKITMALL_DS_META_ORDERS ) as $parcel ) { if ( empty( $parcel['id'] ) ) { continue; } $result = MarkitMall_DS_API::post( '/orders/' . rawurlencode( $parcel['id'] ) . '/cancel', array( 'reason' => __( 'Cancelled in WooCommerce', 'markitmall-dropship' ) ) ); $order->add_order_note( is_wp_error( $result ) ? sprintf( /* translators: %s: error */ __( 'MarkitMall could not cancel the parcel: %s', 'markitmall-dropship' ), $result->get_error_message() ) : __( 'MarkitMall parcel cancelled.', 'markitmall-dropship' ) ); } } /** * Orders with MarkitMall products, newest first, paginated. * * @param string $filter "all", or "attention" for those not sent yet (failed or retrying). * @return object { orders: WC_Order[], total: int, max_num_pages: int } */ public static function query( $filter = 'all', $page = 1, $per_page = 20 ) { $meta = 'attention' === $filter ? array( 'key' => MARKITMALL_DS_META_STATE, 'value' => array( 'failed', 'retrying' ), 'compare' => 'IN' ) : array( 'key' => MARKITMALL_DS_META_STATE, 'value' => 'none', 'compare' => '!=' ); return wc_get_orders( array( 'limit' => $per_page, 'paged' => max( 1, (int) $page ), 'paginate' => true, 'orderby' => 'date', 'order' => 'DESC', 'meta_query' => array( $meta ), // phpcs:ignore WordPress.DB.SlowDBQuery ) ); } /** Where an order is, as a label and a tone for MarkitMall_DS_Admin::badge(). */ private static function state_badge( WC_Order $order ) { $state = (string) $order->get_meta( MARKITMALL_DS_META_STATE ); $parcels = (array) $order->get_meta( MARKITMALL_DS_META_ORDERS ); if ( 'failed' === $state ) { return MarkitMall_DS_Admin::badge( __( 'Not sent', 'markitmall-dropship' ), 'bad' ); } if ( 'retrying' === $state || 'sending' === $state ) { return MarkitMall_DS_Admin::badge( __( 'Retrying', 'markitmall-dropship' ), 'warn' ); } $labels = array( 'RECEIVED' => array( __( 'Received', 'markitmall-dropship' ), 'brand' ), 'AWAITING_FUNDS' => array( __( 'Awaiting funds', 'markitmall-dropship' ), 'warn' ), 'CONFIRMED' => array( __( 'Confirmed', 'markitmall-dropship' ), 'brand' ), 'PACKED' => array( __( 'Packed', 'markitmall-dropship' ), 'brand' ), 'SHIPPED' => array( __( 'Shipped', 'markitmall-dropship' ), 'good' ), 'DELIVERED' => array( __( 'Delivered', 'markitmall-dropship' ), 'good' ), 'CANCELLED' => array( __( 'Cancelled', 'markitmall-dropship' ), 'plain' ), 'RETURNED' => array( __( 'Returned', 'markitmall-dropship' ), 'bad' ), 'RTO' => array( __( 'Refused', 'markitmall-dropship' ), 'bad' ), ); $out = array(); foreach ( array_unique( array_filter( wp_list_pluck( $parcels, 'status' ) ) ) as $status ) { $l = isset( $labels[ $status ] ) ? $labels[ $status ] : array( ucwords( strtolower( str_replace( '_', ' ', $status ) ) ), 'plain' ); $out[] = MarkitMall_DS_Admin::badge( $l[0], $l[1] ); } return $out ? implode( ' ', $out ) : MarkitMall_DS_Admin::badge( __( 'Sent', 'markitmall-dropship' ), 'brand' ); } /** The orders table, on the Orders tab and (compact) on the Dashboard. */ public static function render_table( $orders, $compact = false ) { if ( ! $orders ) { echo '
' . esc_html__( 'No orders yet', 'markitmall-dropship' ) . '
' . esc_html__( 'When a customer buys an imported product, the order goes to MarkitMall and shows here.', 'markitmall-dropship' ) . '
| #get_order_number() ); ?> | get_formatted_billing_full_name() ) ); ?> get_billing_city() ); ?> |
get_formatted_order_total() ); ?> |
|
get_meta( MARKITMALL_DS_META_ERROR ) ); ?> |
get_date_created() ? $order->get_date_created()->date_i18n( get_option( 'date_format' ) ) : '' ); ?> | $order->get_id() ), 'mm-btn mm-btn-small', '', __( 'Sending…', 'markitmall-dropship' ) ); } ?> |
✓
' . esc_html__( 'Not sent yet: it goes to MarkitMall once it is paid (or if it is cash on delivery).', 'markitmall-dropship' ) . '
'; return; } echo '' . esc_html__( 'Not sent to MarkitMall yet.', 'markitmall-dropship' ) . '
'; echo '' . esc_html__( 'Send to MarkitMall', 'markitmall-dropship' ) . ''; return; } if ( ! $state || 'none' === $state ) { echo '' . esc_html__( 'No MarkitMall products in this order.', 'markitmall-dropship' ) . '
'; return; } echo '' . esc_html__( 'Status:', 'markitmall-dropship' ) . ' ' . esc_html( $state ) . '
'; foreach ( $parcels as $p ) { echo '' . esc_html( ( isset( $p['orderNumber'] ) ? $p['orderNumber'] : '' ) . ' — ' . ( isset( $p['status'] ) ? $p['status'] : '' ) ) . '
'; } $tracking = $order->get_meta( '_markitmall_tracking' ); if ( $tracking ) { echo '' . esc_html__( 'Tracking:', 'markitmall-dropship' ) . ' ' . esc_html( $tracking ) . '
'; } if ( in_array( $state, array( 'failed', 'retrying' ), true ) ) { echo '' . esc_html( (string) $order->get_meta( MARKITMALL_DS_META_ERROR ) ) . '
'; echo '' . esc_html__( 'Retry now', 'markitmall-dropship' ) . ''; } } }