'POST', 'callback' => array( __CLASS__, 'handle' ), // Authenticated by the HMAC signature inside handle(). 'permission_callback' => '__return_true', ) ); } public static function handle( WP_REST_Request $request ) { $secret = MarkitMall_DS_Settings::get_all()['webhook_secret']; $raw = $request->get_body(); $signature = (string) $request->get_header( 'x_markitmall_signature' ); $expected = 'sha256=' . hash_hmac( 'sha256', $raw, $secret ); if ( strlen( $secret ) < 32 || ! hash_equals( $expected, $signature ) ) { return new WP_REST_Response( array( 'error' => 'invalid signature' ), 401 ); } $event = json_decode( $raw, true ); if ( ! is_array( $event ) || empty( $event['event'] ) || ! isset( $event['data'] ) ) { return new WP_REST_Response( array( 'error' => 'bad payload' ), 400 ); } // Sandbox events only reach a store using a test key, and vice versa. if ( isset( $event['mode'] ) && $event['mode'] !== MarkitMall_DS_API::mode() ) { return new WP_REST_Response( array( 'ignored' => 'mode mismatch' ), 200 ); } $event_id = (string) $request->get_header( 'x_markitmall_event_id' ); $seen_key = 'markitmall_evt_' . md5( $event_id ); if ( $event_id && get_transient( $seen_key ) ) { return new WP_REST_Response( array( 'duplicate' => true ), 200 ); } try { $type = (string) $event['event']; $data = (array) $event['data']; if ( 0 === strpos( $type, 'order.' ) ) { self::order_event( $type, $data ); } elseif ( 0 === strpos( $type, 'product.' ) ) { self::product_event( $type, $data ); } } catch ( Throwable $e ) { // A 5xx makes MarkitMall retry later, which is what we want. return new WP_REST_Response( array( 'error' => $e->getMessage() ), 500 ); } if ( $event_id ) { set_transient( $seen_key, 1, 2 * DAY_IN_SECONDS ); } return new WP_REST_Response( array( 'ok' => true ), 200 ); } private static function order_event( $type, array $data ) { $order = isset( $data['externalOrderId'] ) ? wc_get_order( absint( $data['externalOrderId'] ) ) : null; if ( ! $order || ! isset( $data['id'] ) ) { return; // Not one of this store's orders (or deleted). } $parcels = (array) $order->get_meta( MARKITMALL_DS_META_ORDERS ); $known = false; foreach ( $parcels as $i => $p ) { if ( isset( $p['id'] ) && $p['id'] === $data['id'] ) { $parcels[ $i ]['status'] = sanitize_text_field( (string) $data['status'] ); $known = true; } } if ( ! $known ) { $parcels[] = array( 'id' => sanitize_text_field( $data['id'] ), 'orderNumber' => isset( $data['orderNumber'] ) ? sanitize_text_field( (string) $data['orderNumber'] ) : '', 'status' => sanitize_text_field( (string) $data['status'] ), ); } $order->update_meta_data( MARKITMALL_DS_META_ORDERS, $parcels ); $label = count( $parcels ) > 1 && isset( $data['parcel']['part'] ) ? sprintf( ' (parcel %d of %d)', (int) $data['parcel']['part'], (int) $data['parcel']['of'] ) : ''; $complete_on = MarkitMall_DS_Settings::get_all()['complete_on']; $all = static function ( $status ) use ( $parcels ) { foreach ( $parcels as $p ) { if ( ! isset( $p['status'] ) || $p['status'] !== $status ) { return false; } } return true; }; switch ( $type ) { case 'order.confirmed': $order->add_order_note( 'MarkitMall: customer confirmed — packing next' . $label . '.' ); break; case 'order.shipped': $tracking = isset( $data['tracking'] ) ? (array) $data['tracking'] : array(); $line = trim( ( isset( $tracking['courier'] ) ? $tracking['courier'] : '' ) . ' ' . ( isset( $tracking['number'] ) ? $tracking['number'] : '' ) ); if ( $line ) { $existing = (string) $order->get_meta( '_markitmall_tracking' ); $order->update_meta_data( '_markitmall_tracking', trim( $existing ? $existing . ', ' . $line : $line ) ); } // Shown to the customer in their order emails. $order->add_order_note( sprintf( 'Your order has shipped%s. Tracking: %s', $label, $line ? $line : '—' ), true ); if ( 'shipped' === $complete_on && $all( 'SHIPPED' ) && $order->has_status( array( 'processing', 'on-hold' ) ) ) { $order->update_status( 'completed', 'MarkitMall: shipped.' ); } break; case 'order.delivered': $margin = isset( $data['margin'] ) ? (int) $data['margin'] : 0; $order->add_order_note( sprintf( 'MarkitMall: delivered%s.%s', $label, 'COD' === ( isset( $data['paymentMode'] ) ? $data['paymentMode'] : '' ) ? sprintf( ' Margin Rs %s, released after the return window.', number_format_i18n( $margin ) ) : '' ) ); if ( 'delivered' === $complete_on && $all( 'DELIVERED' ) && ! $order->has_status( 'completed' ) ) { $order->update_status( 'completed', 'MarkitMall: delivered.' ); } break; case 'order.cancelled': $reason = isset( $data['cancellationReason'] ) && $data['cancellationReason'] ? $data['cancellationReason'] : 'no reason given'; $order->add_order_note( sprintf( 'MarkitMall: cancelled%s — %s.', $label, $reason ) ); if ( $all( 'CANCELLED' ) && ! $order->has_status( array( 'cancelled', 'refunded' ) ) ) { $order->update_meta_data( '_markitmall_cancelled_by_mm', 1 ); $order->save(); $order->update_status( 'cancelled', 'MarkitMall cancelled every parcel.' ); } break; case 'order.returned': $kind = isset( $data['type'] ) && 'rto' === $data['type'] ? 'refused at delivery' : 'returned by the customer'; $charge = isset( $data['charge'] ) ? (int) $data['charge'] : 0; $credit = isset( $data['credit'] ) ? (int) $data['credit'] : 0; $order->add_order_note( sprintf( 'MarkitMall: parcel %s%s.', $kind, $label ) . ( $charge ? sprintf( ' Rs %s charged to your wallet.', number_format_i18n( $charge ) ) : '' ) . ( $credit ? sprintf( ' Rs %s credited to your wallet — refund your customer from it.', number_format_i18n( $credit ) ) : '' ) ); break; case 'order.received': default: break; } $order->save(); } private static function product_event( $type, array $data ) { if ( empty( $data['productId'] ) ) { return; } $local_id = MarkitMall_DS_Catalog::find_local_product( $data['productId'] ); if ( ! $local_id ) { return; } $product = wc_get_product( $local_id ); if ( ! $product ) { return; } if ( 'product.removed' === $type ) { // No longer for sale: take it off the store rather than sell what cannot ship. $product->set_status( 'draft' ); $product->save(); MarkitMall_DS_Sync::flag( sprintf( '“%s” is no longer available from MarkitMall and was unpublished.', $product->get_name() ) ); return; } $remote = isset( $data['product'] ) && is_array( $data['product'] ) ? $data['product'] : null; if ( ! $remote ) { return; } MarkitMall_DS_Sync::apply_remote_product( $product, $remote ); $import_id = get_post_meta( $local_id, MARKITMALL_DS_META_IMPORT, true ); if ( $import_id ) { MarkitMall_DS_API::post( '/imports/' . rawurlencode( $import_id ) . '/sync-result', array( 'status' => 'ok' ) ); } } }