## Discovery ✅ Rajaongkir plugin uses a completely different approach: - Removes standard WooCommerce city/state fields - Adds custom destination dropdown with Select2 search - Stores destination in WooCommerce session (not address fields) - Reads from session during shipping calculation ## Root Cause of Issues: ### 1. Same rates for different provinces - OrderForm sends: city="Bandung", state="Jawa Barat" - Rajaongkir ignores these fields - Rajaongkir reads: WC()->session->get("selected_destination_id") - Session empty → Uses cached/default rates ### 2. No Rajaongkir API hits - No destination_id in session - Rajaongkir can't calculate without destination - Returns empty or cached rates ## Backend Fix (✅ DONE): Added Rajaongkir session support in calculate_shipping: ```php // Support for Rajaongkir plugin if ( $country === 'ID' && ! empty( $shipping['destination_id'] ) ) { WC()->session->set( 'selected_destination_id', $shipping['destination_id'] ); WC()->session->set( 'selected_destination_label', $shipping['destination_label'] ); } ``` ## Frontend Fix (TODO): Need to add Rajaongkir destination field: 1. Add destination search component (Select2/Combobox) 2. Search Rajaongkir API for locations 3. Pass destination_id to backend 4. Backend sets session before calculate_shipping() ## Documentation: Created RAJAONGKIR_INTEGRATION.md with: - How Rajaongkir works - Why our implementation fails - Complete solution steps - Testing checklist ## Next Steps: 1. Add Rajaongkir search endpoint to OrdersController 2. Create destination search component in OrderForm 3. Pass destination_id in shipping data 4. Test with real Rajaongkir API
2114 lines
85 KiB
PHP
2114 lines
85 KiB
PHP
<?php
|
||
namespace WooNooW\Api;
|
||
|
||
use WP_REST_Request;
|
||
use WP_REST_Response;
|
||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||
|
||
class OrdersController {
|
||
public static function init() {
|
||
// Register action hook for delayed email sending
|
||
add_action( 'woonoow/send_order_email', [ __CLASS__, 'send_order_email' ], 10, 1 );
|
||
|
||
// Hook into order status changes to schedule our delayed emails
|
||
add_action( 'woocommerce_order_status_changed', [ __CLASS__, 'on_order_status_changed' ], 10, 4 );
|
||
}
|
||
|
||
public static function register() {
|
||
register_rest_route('woonoow/v1', '/orders', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'index'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'status' => ['type' => 'string', 'required' => false], // comma-separated
|
||
'page' => ['type' => 'integer', 'default' => 1],
|
||
'per_page' => ['type' => 'integer', 'default' => 25],
|
||
'date_start' => ['type' => 'string', 'required' => false], // YYYY-MM-DD or ISO8601
|
||
'date_end' => ['type' => 'string', 'required' => false],
|
||
'orderby' => ['type' => 'string', 'default' => 'date'],
|
||
'order' => ['type' => 'string', 'default' => 'desc'], // asc|desc
|
||
],
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/orders/(?P<id>\d+)', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'show'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/orders/(?P<id>\d+)', [
|
||
'methods' => 'PATCH',
|
||
'callback' => [__CLASS__, 'update'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'items' => [ 'type' => 'array', 'required' => false ],
|
||
'billing' => [ 'type' => 'object', 'required' => false ],
|
||
'shipping' => [ 'type' => 'object', 'required' => false ],
|
||
'status' => [ 'type' => 'string', 'required' => false ],
|
||
'payment_method' => [ 'type' => 'string', 'required' => false ],
|
||
'shipping_method' => [ 'type' => 'string', 'required' => false ],
|
||
'coupons' => [ 'type' => 'array', 'required' => false ],
|
||
'note' => [ 'type' => 'string', 'required' => false ],
|
||
],
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/orders', [
|
||
'methods' => 'POST',
|
||
'callback' => [__CLASS__, 'create'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'items' => [ 'type' => 'array', 'required' => true ],
|
||
'billing' => [ 'type' => 'object', 'required' => false ],
|
||
'shipping' => [ 'type' => 'object', 'required' => false ],
|
||
'status' => [ 'type' => 'string', 'required' => false ],
|
||
'payment_method' => [ 'type' => 'string', 'required' => false ],
|
||
'shipping_method' => [ 'type' => 'string', 'required' => false ], // e.g. flat_rate:1
|
||
'coupons' => [ 'type' => 'array', 'required' => false ], // array of codes
|
||
'note' => [ 'type' => 'string', 'required' => false ],
|
||
],
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/orders/(?P<id>\d+)', [
|
||
'methods' => 'DELETE',
|
||
'callback' => [__CLASS__, 'delete'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
]);
|
||
|
||
// Lightweight product search to help build orders from admin UI
|
||
register_rest_route('woonoow/v1', '/products', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'products'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'search' => [ 'type' => 'string', 'required' => false ],
|
||
'limit' => [ 'type' => 'integer', 'default' => 10 ],
|
||
],
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/payments', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'payments'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/shippings', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'shippings'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
]);
|
||
|
||
// Calculate shipping rates for given address and cart
|
||
register_rest_route('woonoow/v1', '/shipping/calculate', [
|
||
'methods' => 'POST',
|
||
'callback' => [__CLASS__, 'calculate_shipping'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'items' => [ 'type' => 'array', 'required' => true ],
|
||
'shipping' => [ 'type' => 'object', 'required' => true ],
|
||
],
|
||
]);
|
||
|
||
// Calculate order preview with taxes and totals
|
||
register_rest_route('woonoow/v1', '/orders/preview', [
|
||
'methods' => 'POST',
|
||
'callback' => [__CLASS__, 'preview_order'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'items' => [ 'type' => 'array', 'required' => true ],
|
||
'billing' => [ 'type' => 'object', 'required' => false ],
|
||
'shipping' => [ 'type' => 'object', 'required' => false ],
|
||
'shipping_method' => [ 'type' => 'string', 'required' => false ],
|
||
'coupons' => [ 'type' => 'array', 'required' => false ],
|
||
],
|
||
]);
|
||
|
||
// Retry payment processing for an order
|
||
register_rest_route('woonoow/v1', '/orders/(?P<id>\d+)/retry-payment', [
|
||
'methods' => 'POST',
|
||
'callback' => [__CLASS__, 'retry_payment'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
]);
|
||
|
||
// Validate coupon code
|
||
register_rest_route('woonoow/v1', '/coupons/validate', [
|
||
'methods' => 'POST',
|
||
'callback' => [__CLASS__, 'validate_coupon'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'code' => [
|
||
'required' => true,
|
||
'type' => 'string',
|
||
],
|
||
'items' => [
|
||
'required' => false,
|
||
'type' => 'array',
|
||
],
|
||
'subtotal' => [
|
||
'required' => false,
|
||
'type' => 'number',
|
||
],
|
||
],
|
||
]);
|
||
|
||
register_rest_route('woonoow/v1', '/countries', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'countries'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
]);
|
||
|
||
// Customer search for autofill
|
||
register_rest_route('woonoow/v1', '/customers/search', [
|
||
'methods' => 'GET',
|
||
'callback' => [__CLASS__, 'search_customers'],
|
||
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
|
||
'args' => [
|
||
'email' => [
|
||
'required' => false,
|
||
'type' => 'string',
|
||
],
|
||
'search' => [
|
||
'required' => false,
|
||
'type' => 'string',
|
||
],
|
||
],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* GET /woonoow/v1/orders
|
||
*/
|
||
public static function index( WP_REST_Request $req ) : WP_REST_Response {
|
||
$page = max( 1, absint( $req->get_param( 'page' ) ?? 1 ) );
|
||
$per_page = max( 1, min( 200, absint( $req->get_param( 'per_page' ) ?? 20 ) ) );
|
||
|
||
$orderby = sanitize_key( $req->get_param( 'orderby' ) ?? 'date' );
|
||
$order = strtoupper( $req->get_param( 'order' ) ?? 'DESC' );
|
||
if ( ! in_array( $orderby, [ 'date', 'id', 'modified', 'total' ], true ) ) $orderby = 'date';
|
||
if ( $order !== 'ASC' ) $order = 'DESC';
|
||
|
||
$status = $req->get_param( 'status' );
|
||
// If status is missing, include ALL Woo statuses.
|
||
$statuses = $status
|
||
? array_map( function( $s ) { $s = sanitize_key( trim( $s ) ); return 'wc-' . preg_replace( '/^wc-/', '', $s ); }, explode( ',', $status ) )
|
||
: array_keys( wc_get_order_statuses() );
|
||
|
||
// --- Build date range (HPOS-safe, string values) ---
|
||
$date_start = $req->get_param( 'date_start' );
|
||
$date_end = $req->get_param( 'date_end' );
|
||
|
||
$after_str = null;
|
||
$before_str = null;
|
||
|
||
if ( $date_start ) {
|
||
// Expecting YYYY-MM-DD; build 00:00:00 UTC
|
||
$after_ts = strtotime( $date_start . ' 00:00:00' );
|
||
if ( $after_ts ) $after_str = gmdate( 'Y-m-d H:i:s', $after_ts );
|
||
}
|
||
if ( $date_end ) {
|
||
// Build 23:59:59 UTC
|
||
$before_ts = strtotime( $date_end . ' 23:59:59' );
|
||
if ( $before_ts ) $before_str = gmdate( 'Y-m-d H:i:s', $before_ts );
|
||
}
|
||
|
||
$date_created = null;
|
||
if ( $after_str || $before_str ) {
|
||
$date_created = array_filter( [
|
||
'after' => $after_str,
|
||
'before' => $before_str,
|
||
// note: do NOT include 'inclusive' to avoid Woo date arg coercion issues
|
||
] );
|
||
}
|
||
|
||
$args = [
|
||
'paginate' => true,
|
||
'page' => $page,
|
||
'limit' => $per_page,
|
||
'status' => $statuses,
|
||
'orderby' => $orderby === 'total' ? 'total' : $orderby, // Woo supports 'total' in recent versions
|
||
'order' => $order,
|
||
'return' => 'ids', // HPOS friendly + fast
|
||
];
|
||
|
||
// Guard: Some Woo installs throw when passing array date ranges into OrdersTableQuery.
|
||
// To be safe, only pass a single-bound filter directly. For two-sided ranges, we post-filter below.
|
||
$pass_single_bound = $date_created && (
|
||
( isset($date_created['after']) && ! isset($date_created['before']) ) ||
|
||
( isset($date_created['before']) && ! isset($date_created['after']) )
|
||
);
|
||
if ( $pass_single_bound ) {
|
||
$args['date_created'] = $date_created; // single bound only
|
||
}
|
||
|
||
// Optional simple ID search
|
||
$search = $req->get_param( 'search' );
|
||
if ( $search && preg_match( '/^#?(\\d+)$/', $search, $m ) ) {
|
||
$args['include'] = [ absint( $m[1] ) ];
|
||
}
|
||
|
||
$result = wc_get_orders( $args );
|
||
|
||
// If we had a two-sided date range, filter results manually to avoid OrdersTableQuery fatal.
|
||
if ( $date_created && isset($date_created['after']) && isset($date_created['before']) ) {
|
||
$after_ts = strtotime( $date_created['after'] );
|
||
$before_ts = strtotime( $date_created['before'] );
|
||
if ( $after_ts && $before_ts ) {
|
||
$result->orders = array_values( array_filter( $result->orders, function( $order_id ) use ( $after_ts, $before_ts ) {
|
||
$o = wc_get_order( $order_id );
|
||
if ( ! $o || ! $o->get_date_created() ) return false;
|
||
$t = $o->get_date_created()->getTimestamp();
|
||
return ( $t >= $after_ts && $t <= $before_ts );
|
||
}));
|
||
$result->total = count( $result->orders );
|
||
}
|
||
}
|
||
|
||
$rows = array_map( function( $order_id ) {
|
||
$o = wc_get_order( $order_id );
|
||
|
||
// Customer name: prefer billing first/last; otherwise WP user's display name; otherwise null
|
||
$first = trim( (string) $o->get_billing_first_name() );
|
||
$last = trim( (string) $o->get_billing_last_name() );
|
||
$name = trim( $first . ' ' . $last );
|
||
if ( $name === '' ) {
|
||
$uid = $o->get_user_id();
|
||
if ( $uid ) {
|
||
$u = get_userdata( $uid );
|
||
if ( $u && ! empty( $u->display_name ) ) {
|
||
$name = (string) $u->display_name;
|
||
}
|
||
}
|
||
if ( $name === '' ) {
|
||
$name = null; // no email/Guest fallback per requirement
|
||
}
|
||
}
|
||
|
||
// Items count (qty-based) and brief label
|
||
$items = $o->get_items();
|
||
$items_count = 0;
|
||
$brief_parts = [];
|
||
$i = 0;
|
||
foreach ( $items as $it ) {
|
||
$qty = (int) $it->get_quantity();
|
||
$items_count += $qty;
|
||
if ( $i < 2 ) {
|
||
$brief_parts[] = sprintf( '%s ×%d', $it->get_name(), $qty );
|
||
}
|
||
$i++;
|
||
}
|
||
$items_brief = implode( ', ', $brief_parts );
|
||
$lines_total = count( $items );
|
||
$lines_preview = min( 2, $lines_total );
|
||
if ( $lines_total > 2 ) {
|
||
$items_brief .= sprintf( ' +%d more', $lines_total - 2 );
|
||
}
|
||
|
||
// Full list (name ×qty), used by hover card
|
||
$full_parts = [];
|
||
foreach ( $items as $it_full ) {
|
||
$full_parts[] = sprintf( '%s ×%d', $it_full->get_name(), (int) $it_full->get_quantity() );
|
||
}
|
||
$items_full = implode( ', ', $full_parts );
|
||
|
||
// Build site-local ISO and epoch seconds for created date
|
||
$dt_created = $o->get_date_created();
|
||
$date_ts = $dt_created ? $dt_created->getTimestamp() : null; // epoch seconds (UTC)
|
||
$tz_obj = wp_timezone();
|
||
$tz_str = wp_timezone_string();
|
||
$date_iso = null;
|
||
if ( $dt_created ) {
|
||
$local = clone $dt_created;
|
||
$local = $local->setTimezone( $tz_obj );
|
||
$date_iso = $local->format( DATE_ATOM ); // e.g. 2025-10-25T00:23:53+07:00
|
||
}
|
||
|
||
return [
|
||
'id' => $o->get_id(),
|
||
'number' => $o->get_order_number(),
|
||
'date' => $date_iso,
|
||
'date_ts' => $date_ts,
|
||
'tz' => $tz_str,
|
||
'status' => $o->get_status(),
|
||
'total' => (float) $o->get_total(),
|
||
'currency' => $o->get_currency(),
|
||
'currency_symbol' => get_woocommerce_currency_symbol( $o->get_currency() ),
|
||
'customer' => $name,
|
||
'items_count' => $items_count,
|
||
'items_brief' => $items_brief,
|
||
'items_full' => $items_full,
|
||
'lines_total' => $lines_total,
|
||
'lines_preview' => $lines_preview,
|
||
];
|
||
}, $result->orders );
|
||
|
||
return new WP_REST_Response( [
|
||
'rows' => $rows,
|
||
'total' => (int) $result->total,
|
||
'page' => (int) $page,
|
||
'per_page' => (int) $per_page,
|
||
], 200 );
|
||
}
|
||
|
||
/**
|
||
* GET /woonoow/v1/orders/{id}
|
||
*/
|
||
public static function show( WP_REST_Request $req ) : WP_REST_Response {
|
||
$id = absint( $req['id'] );
|
||
if ( ! $id ) return new WP_REST_Response( [ 'error' => 'invalid_id' ], 400 );
|
||
|
||
$order = wc_get_order( $id );
|
||
if ( ! $order ) return new WP_REST_Response( [ 'error' => 'not_found' ], 404 );
|
||
|
||
$items = [];
|
||
foreach ( $order->get_items() as $item ) {
|
||
/** @var \WC_Order_Item_Product $item */
|
||
$product = $item->get_product();
|
||
$qty = (int) $item->get_quantity();
|
||
$subtotal = (float) $item->get_subtotal();
|
||
$total = (float) $item->get_total();
|
||
$product_id = $product ? $product->get_id() : 0;
|
||
$unit_price = $qty > 0 ? (float) ( $subtotal / $qty ) : 0.0;
|
||
$items[] = [
|
||
'line_item_id' => $item->get_id(),
|
||
'id' => $item->get_id(),
|
||
'product_id' => $product_id,
|
||
'name' => $item->get_name(),
|
||
'sku' => $product ? $product->get_sku() : '',
|
||
'qty' => $qty,
|
||
'price' => $unit_price,
|
||
'subtotal' => $subtotal,
|
||
'total' => $total,
|
||
'subtotal_fmt' => wc_price( $subtotal, [ 'currency' => $order->get_currency() ] ),
|
||
'total_fmt' => wc_price( $total, [ 'currency' => $order->get_currency() ] ),
|
||
'virtual' => $product ? $product->is_virtual() : false,
|
||
'downloadable' => $product ? $product->is_downloadable() : false,
|
||
'regular_price' => $product ? (float) $product->get_regular_price() : 0,
|
||
'sale_price' => $product && $product->get_sale_price() ? (float) $product->get_sale_price() : null,
|
||
];
|
||
}
|
||
|
||
$notes = [];
|
||
foreach ( wc_get_order_notes( [ 'order_id' => $id, 'orderby' => 'date_created', 'order' => 'DESC' ] ) as $note ) {
|
||
$notes[] = [
|
||
'id' => $note->id,
|
||
'content' => $note->content,
|
||
'is_customer_note' => (bool) $note->customer_note,
|
||
'date' => $note->date_created ? $note->date_created->date( DATE_ATOM ) : null,
|
||
];
|
||
}
|
||
|
||
$coupon_codes = [];
|
||
foreach ( $order->get_items( 'coupon' ) as $citem ) {
|
||
$coupon_codes[] = (string) $citem->get_code();
|
||
}
|
||
|
||
$dt_created = $order->get_date_created();
|
||
$date_ts = $dt_created ? $dt_created->getTimestamp() : null;
|
||
$tz_obj = wp_timezone();
|
||
$tz_str = wp_timezone_string();
|
||
$date_iso = null;
|
||
if ( $dt_created ) {
|
||
$local = clone $dt_created;
|
||
$local = $local->setTimezone( $tz_obj );
|
||
$date_iso = $local->format( DATE_ATOM );
|
||
}
|
||
|
||
// Defensive address formatting (Woo helpers may not be loaded in rare contexts)
|
||
$billing_addr_arr = $order->get_address( 'billing' );
|
||
$shipping_addr_arr = $order->get_address( 'shipping' );
|
||
|
||
// Build raw billing/shipping fields for editing
|
||
$billing_fields = [
|
||
'first_name' => (string) $order->get_billing_first_name(),
|
||
'last_name' => (string) $order->get_billing_last_name(),
|
||
'email' => (string) $order->get_billing_email(),
|
||
'phone' => (string) $order->get_billing_phone(),
|
||
'address_1' => (string) $order->get_billing_address_1(),
|
||
'address_2' => (string) $order->get_billing_address_2(),
|
||
'city' => (string) $order->get_billing_city(),
|
||
'state' => (string) $order->get_billing_state(),
|
||
'postcode' => (string) $order->get_billing_postcode(),
|
||
'country' => (string) $order->get_billing_country(),
|
||
];
|
||
if ( $billing_fields['first_name'] === '' && $billing_fields['last_name'] === '' ) {
|
||
$uid = $order->get_user_id();
|
||
if ( $uid ) {
|
||
$u = get_userdata( $uid );
|
||
if ( $u && ! empty( $u->display_name ) ) {
|
||
$parts = preg_split('/\s+/', trim( (string) $u->display_name ), 2);
|
||
$billing_fields['first_name'] = (string) ( $parts[0] ?? '' );
|
||
$billing_fields['last_name'] = (string) ( $parts[1] ?? '' );
|
||
}
|
||
}
|
||
}
|
||
|
||
$shipping_fields = [
|
||
'first_name' => (string) $order->get_shipping_first_name(),
|
||
'last_name' => (string) $order->get_shipping_last_name(),
|
||
'phone' => '', // Woo core doesn't store shipping phone by default
|
||
'address_1' => (string) $order->get_shipping_address_1(),
|
||
'address_2' => (string) $order->get_shipping_address_2(),
|
||
'city' => (string) $order->get_shipping_city(),
|
||
'state' => (string) $order->get_shipping_state(),
|
||
'postcode' => (string) $order->get_shipping_postcode(),
|
||
'country' => (string) $order->get_shipping_country(),
|
||
];
|
||
|
||
if ( function_exists('wc') && is_object( wc() ) && isset( wc()->countries ) && method_exists( wc()->countries, 'get_formatted_address' ) ) {
|
||
$billing_addr_html = wc()->countries->get_formatted_address( $billing_addr_arr );
|
||
$shipping_addr_html = wc()->countries->get_formatted_address( $shipping_addr_arr );
|
||
} elseif ( function_exists('wc_format_address') ) {
|
||
$billing_addr_html = \wc_format_address( $billing_addr_arr );
|
||
$shipping_addr_html = \wc_format_address( $shipping_addr_arr );
|
||
} else {
|
||
// Fallback: simple HTML join
|
||
$fmt = function( $a ) {
|
||
$parts = [
|
||
trim( ($a['first_name'] ?? '') . ' ' . ($a['last_name'] ?? '') ),
|
||
$a['address_1'] ?? '',
|
||
$a['address_2'] ?? '',
|
||
trim( (($a['city'] ?? '')) . ( ($a['state']??'') ? ', ' . $a['state'] : '' ) . ( ($a['postcode']??'') ? ' ' . $a['postcode'] : '' ) ),
|
||
$a['country'] ?? '',
|
||
];
|
||
$parts = array_values( array_filter( $parts, fn($v) => $v !== '' ) );
|
||
return implode('<br>', $parts);
|
||
};
|
||
$billing_addr_html = $fmt( $billing_addr_arr );
|
||
$shipping_addr_html = $fmt( $shipping_addr_arr );
|
||
}
|
||
|
||
$data = [
|
||
'id' => $order->get_id(),
|
||
'number' => $order->get_order_number(),
|
||
'status' => $order->get_status(),
|
||
'currency' => $order->get_currency(),
|
||
'currency_symbol' => get_woocommerce_currency_symbol( $order->get_currency() ),
|
||
'currency_display' => get_woocommerce_currency_symbol( $order->get_currency() ),
|
||
'currency_pos' => get_option('woocommerce_currency_pos', 'left'),
|
||
'decimals' => wc_get_price_decimals(),
|
||
'decimal_sep' => wc_get_price_decimal_separator(),
|
||
'thousand_sep' => wc_get_price_thousand_separator(),
|
||
'date' => $date_iso,
|
||
'date_ts' => $date_ts,
|
||
'tz' => $tz_str,
|
||
'payment_method' => self::get_payment_method_title($order),
|
||
'payment_method_id' => $order->get_payment_method(),
|
||
'payment_meta' => self::get_payment_metadata($order),
|
||
'shipping_method' => self::get_shipping_method_title($order),
|
||
'shipping_method_id' => self::get_shipping_method_id($order),
|
||
'totals' => [
|
||
'subtotal' => (float) $order->get_subtotal(),
|
||
'discount' => (float) $order->get_discount_total(),
|
||
'shipping' => (float) $order->get_shipping_total(),
|
||
'tax' => (float) $order->get_total_tax(),
|
||
'total' => (float) $order->get_total(),
|
||
'subtotal_fmt' => wc_price( (float) $order->get_subtotal(), [ 'currency' => $order->get_currency() ] ),
|
||
'discount_fmt' => wc_price( (float) $order->get_discount_total(), [ 'currency' => $order->get_currency() ] ),
|
||
'shipping_fmt' => wc_price( (float) $order->get_shipping_total(), [ 'currency' => $order->get_currency() ] ),
|
||
'tax_fmt' => wc_price( (float) $order->get_total_tax(), [ 'currency' => $order->get_currency() ] ),
|
||
'total_fmt' => wc_price( (float) $order->get_total(), [ 'currency' => $order->get_currency() ] ),
|
||
],
|
||
'billing' => array_merge( $billing_fields, [
|
||
'name' => trim( $billing_fields['first_name'] . ' ' . $billing_fields['last_name'] ),
|
||
'email' => $billing_fields['email'],
|
||
'phone' => $billing_fields['phone'],
|
||
'address_html' => $billing_addr_html,
|
||
]),
|
||
'shipping' => array_merge( $shipping_fields, [
|
||
'name' => trim( $shipping_fields['first_name'] . ' ' . $shipping_fields['last_name'] ),
|
||
'address_html' => $shipping_addr_html,
|
||
]),
|
||
'items' => $items,
|
||
'notes' => $notes,
|
||
'coupons' => $coupon_codes,
|
||
'customer_note' => $order->get_customer_note(),
|
||
];
|
||
|
||
return new WP_REST_Response( $data, 200 );
|
||
}
|
||
|
||
/**
|
||
* PATCH /woonoow/v1/orders/{id}
|
||
* Upserts line items, replaces coupon lines, updates addresses & meta, then recalculates totals.
|
||
*/
|
||
public static function update( WP_REST_Request $req ) : WP_REST_Response {
|
||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||
return new \WP_REST_Response( [ 'error' => 'forbidden' ], 403 );
|
||
}
|
||
|
||
$id = absint( $req['id'] );
|
||
if ( ! $id ) return new \WP_REST_Response( [ 'error' => 'invalid_id' ], 400 );
|
||
|
||
$order = wc_get_order( $id );
|
||
if ( ! $order ) return new \WP_REST_Response( [ 'error' => 'not_found' ], 404 );
|
||
|
||
$p = $req->get_json_params();
|
||
|
||
$items = is_array( $p['items'] ?? null ) ? $p['items'] : null; // null means no change
|
||
$billing = is_array( $p['billing'] ?? null ) ? $p['billing'] : null;
|
||
$shipping = is_array( $p['shipping'] ?? null ) ? $p['shipping'] : null;
|
||
$status = array_key_exists('status', $p) ? sanitize_text_field( (string) $p['status'] ) : null;
|
||
$payment_method = array_key_exists('payment_method', $p) ? sanitize_text_field( (string) $p['payment_method'] ) : null;
|
||
$shipping_method = array_key_exists('shipping_method', $p) ? sanitize_text_field( (string) $p['shipping_method'] ) : null;
|
||
$coupons = array_key_exists('coupons', $p) ? array_filter( array_map( 'sanitize_text_field', (array) $p['coupons'] ) ) : null;
|
||
$note = array_key_exists('customer_note', $p) ? wp_kses_post( (string) $p['customer_note'] ) : null;
|
||
|
||
try {
|
||
// === Items upsert ===
|
||
if ( is_array( $items ) ) {
|
||
// Map existing line items by id for quick lookup
|
||
$existing_items = $order->get_items( 'line_item' );
|
||
$existing_map = [];
|
||
foreach ( $existing_items as $it ) { $existing_map[ $it->get_id() ] = $it; }
|
||
|
||
$keep_ids = [];
|
||
foreach ( $items as $row ) {
|
||
$li_id = absint( $row['line_item_id'] ?? 0 );
|
||
$qty = max( 1, absint( $row['qty'] ?? 1 ) );
|
||
if ( $li_id && isset( $existing_map[$li_id] ) ) {
|
||
// Update quantity of existing line
|
||
$existing_map[$li_id]->set_quantity( $qty );
|
||
$keep_ids[] = $li_id;
|
||
} else {
|
||
$pid = absint( $row['product_id'] ?? 0 );
|
||
if ( $pid ) {
|
||
$product = wc_get_product( $pid );
|
||
if ( $product ) {
|
||
$order->add_product( $product, $qty );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Delete removed items (present before, missing now)
|
||
foreach ( $existing_items as $it ) {
|
||
if ( ! in_array( $it->get_id(), $keep_ids, true ) ) {
|
||
$order->remove_item( $it->get_id() );
|
||
}
|
||
}
|
||
}
|
||
|
||
// === Billing ===
|
||
if ( is_array( $billing ) ) {
|
||
$order->set_address( [
|
||
'first_name' => (string) ( $billing['first_name'] ?? '' ),
|
||
'last_name' => (string) ( $billing['last_name'] ?? '' ),
|
||
'email' => (string) ( $billing['email'] ?? '' ),
|
||
'phone' => (string) ( $billing['phone'] ?? '' ),
|
||
'address_1' => (string) ( $billing['address_1'] ?? '' ),
|
||
'address_2' => (string) ( $billing['address_2'] ?? '' ),
|
||
'city' => (string) ( $billing['city'] ?? '' ),
|
||
'state' => (string) ( $billing['state'] ?? '' ),
|
||
'postcode' => (string) ( $billing['postcode'] ?? '' ),
|
||
'country' => (string) ( $billing['country'] ?? '' ),
|
||
], 'billing' );
|
||
}
|
||
|
||
// === Shipping ===
|
||
if ( is_array( $shipping ) ) {
|
||
$order->set_address( [
|
||
'first_name' => (string) ( $shipping['first_name'] ?? ( $shipping['name'] ?? '' ) ),
|
||
'last_name' => (string) ( $shipping['last_name'] ?? '' ),
|
||
'phone' => (string) ( $shipping['phone'] ?? '' ),
|
||
'address_1' => (string) ( $shipping['address_1'] ?? '' ),
|
||
'address_2' => (string) ( $shipping['address_2'] ?? '' ),
|
||
'city' => (string) ( $shipping['city'] ?? '' ),
|
||
'state' => (string) ( $shipping['state'] ?? '' ),
|
||
'postcode' => (string) ( $shipping['postcode'] ?? '' ),
|
||
'country' => (string) ( $shipping['country'] ?? '' ),
|
||
], 'shipping' );
|
||
}
|
||
|
||
// === Shipping method line ===
|
||
if ( null !== $shipping_method ) {
|
||
// Remove existing shipping items
|
||
foreach ( $order->get_items( 'shipping' ) as $ship_item ) { $order->remove_item( $ship_item->get_id() ); }
|
||
if ( $shipping_method ) {
|
||
$method_id = $shipping_method;
|
||
$instance_id = null;
|
||
if ( strpos( $shipping_method, ':' ) !== false ) {
|
||
list( $method_id, $instance_str ) = explode( ':', $shipping_method, 2 );
|
||
$instance_id = absint( $instance_str );
|
||
}
|
||
$ship_item = new \WC_Order_Item_Shipping();
|
||
$ship_item->set_method_id( $method_id );
|
||
if ( null !== $instance_id && method_exists( $ship_item, 'set_instance_id' ) ) {
|
||
$ship_item->set_instance_id( $instance_id );
|
||
}
|
||
$ship_item->set_method_title( $method_id );
|
||
$ship_item->set_total( 0 );
|
||
$order->add_item( $ship_item );
|
||
}
|
||
}
|
||
|
||
// === Payment method (info only) ===
|
||
if ( null !== $payment_method ) {
|
||
if ( $payment_method ) { $order->set_payment_method( $payment_method ); }
|
||
}
|
||
|
||
// === Coupons: replace ===
|
||
if ( is_array( $coupons ) ) {
|
||
// Remove existing coupon lines
|
||
foreach ( $order->get_items( 'coupon' ) as $citem ) { $order->remove_item( $citem->get_id() ); }
|
||
foreach ( $coupons as $code ) {
|
||
try {
|
||
$coupon = new \WC_Coupon( $code );
|
||
if ( $coupon && $coupon->get_code() ) {
|
||
$ci = new \WC_Order_Item_Coupon();
|
||
$ci->set_code( $coupon->get_code() );
|
||
$order->add_item( $ci );
|
||
}
|
||
} catch ( \Throwable $e ) { /* ignore invalid */ }
|
||
}
|
||
}
|
||
|
||
// Customer note (visible to customer) - allow empty to clear note
|
||
if ( null !== $note ) {
|
||
$order->set_customer_note( $note );
|
||
}
|
||
|
||
// Totals & status
|
||
// Only recalculate if items/coupons/shipping changed; skip for status-only updates
|
||
$needs_recalc = is_array( $items ) || is_array( $coupons ) || null !== $shipping_method;
|
||
if ( $needs_recalc ) {
|
||
$order->calculate_totals( true );
|
||
}
|
||
|
||
// PROFILING: Find what's causing the 30s delay
|
||
if ( null !== $status && $status !== '' ) {
|
||
try {
|
||
$order->set_status( $status, 'Updated via API' );
|
||
} catch ( \Throwable $e ) {
|
||
/* ignore invalid */
|
||
}
|
||
}
|
||
|
||
// SOLUTION: Block WooCommerce analytics tracking (pixel.wp.com) during save
|
||
// This was causing 30s timeout on every order status change
|
||
add_filter( 'pre_http_request', function( $preempt, $args, $url ) {
|
||
// Block WooCommerce analytics/tracking requests
|
||
if ( strpos( $url, 'pixel.wp.com' ) !== false || strpos( $url, 'stats.wp.com' ) !== false ) {
|
||
return new \WP_Error( 'http_request_blocked', 'WooCommerce analytics blocked during order save' );
|
||
}
|
||
return $preempt;
|
||
}, PHP_INT_MAX, 3 );
|
||
|
||
$order->save();
|
||
|
||
// Clean up
|
||
remove_all_filters( 'pre_http_request' );
|
||
|
||
// Schedule email for later
|
||
if ( null !== $status && $status !== '' ) {
|
||
$order_id = $order->get_id();
|
||
add_action( 'shutdown', function() use ( $order_id, $status ) {
|
||
error_log('[WooNooW] Shutdown hook firing - scheduling email for order #' . $order_id);
|
||
self::schedule_order_email( $order_id, $status );
|
||
error_log('[WooNooW] Email scheduled successfully for order #' . $order_id);
|
||
}, 999 );
|
||
}
|
||
|
||
return new \WP_REST_Response( [ 'ok' => true, 'id' => $order->get_id() ], 200 );
|
||
} catch ( \Throwable $e ) {
|
||
// Log the actual error for debugging
|
||
error_log('[WooNooW] Order update failed: ' . $e->getMessage());
|
||
|
||
// Return user-friendly error message
|
||
return new \WP_REST_Response( [
|
||
'error' => 'update_failed',
|
||
'message' => __( 'Unable to update order. Please check all fields and try again.', 'woonoow' )
|
||
], 500 );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hook handler for order status changes.
|
||
* Schedules email notification with 15s delay.
|
||
* ONLY runs outside of API requests (API requests schedule manually).
|
||
*/
|
||
public static function on_order_status_changed( $order_id, $status_from, $status_to, $order ) {
|
||
// Skip if we're in an API request (we schedule manually there)
|
||
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
|
||
error_log('[WooNooW] Skipping auto-schedule during API request for order #' . $order_id);
|
||
return;
|
||
}
|
||
|
||
// Schedule email notification with 15s delay
|
||
self::schedule_order_email( $order_id, $status_to );
|
||
error_log('[WooNooW] Order #' . $order_id . ' status changed: ' . $status_from . ' → ' . $status_to . ', email scheduled');
|
||
}
|
||
|
||
/**
|
||
* Schedule order email notification with 15s delay (non-blocking).
|
||
*/
|
||
private static function schedule_order_email( int $order_id, string $status ) {
|
||
if ( function_exists( 'as_schedule_single_action' ) ) {
|
||
// Use Action Scheduler (preferred)
|
||
as_schedule_single_action( time() + 15, 'woonoow/send_order_email', [ 'order_id' => $order_id, 'status' => $status ], 'woonoow-emails' );
|
||
} else {
|
||
// Fallback to wp-cron
|
||
wp_schedule_single_event( time() + 15, 'woonoow/send_order_email', [ 'order_id' => $order_id, 'status' => $status ] );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* POST /woonoow/v1/orders
|
||
*/
|
||
public static function create( WP_REST_Request $req ) : WP_REST_Response {
|
||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||
return new \WP_REST_Response( [ 'error' => 'forbidden' ], 403 );
|
||
}
|
||
|
||
$p = $req->get_json_params();
|
||
|
||
$items = is_array( $p['items'] ?? null ) ? $p['items'] : [];
|
||
$billing = is_array( $p['billing'] ?? null ) ? $p['billing'] : [];
|
||
$shipping = is_array( $p['shipping'] ?? null ) ? $p['shipping'] : [];
|
||
$status = sanitize_text_field( $p['status'] ?? 'pending' );
|
||
$payment_method = sanitize_text_field( $p['payment_method'] ?? '' );
|
||
$shipping_method = sanitize_text_field( $p['shipping_method'] ?? '' ); // e.g. flat_rate:1
|
||
$coupons = array_filter( array_map( 'sanitize_text_field', (array) ( $p['coupons'] ?? [] ) ) );
|
||
$note = isset( $p['customer_note'] ) ? wp_kses_post( (string) $p['customer_note'] ) : '';
|
||
$register_member = (bool) ( $p['register_as_member'] ?? false );
|
||
|
||
// Validation: Collect all missing required fields
|
||
$validation_errors = [];
|
||
|
||
// 1. At least one product is required
|
||
if ( empty( $items ) ) {
|
||
$validation_errors[] = __( 'At least one product is required', 'woonoow' );
|
||
}
|
||
|
||
// 2. Check if all products are virtual/downloadable
|
||
$has_physical_product = false;
|
||
foreach ( $items as $item ) {
|
||
$product_id = absint( $item['product_id'] ?? 0 );
|
||
if ( $product_id ) {
|
||
$product = wc_get_product( $product_id );
|
||
if ( $product && ! $product->is_virtual() && ! $product->is_downloadable() ) {
|
||
$has_physical_product = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. Billing information is required for a healthy order
|
||
$required_billing_fields = [
|
||
'first_name' => __( 'Billing first name', 'woonoow' ),
|
||
'last_name' => __( 'Billing last name', 'woonoow' ),
|
||
'email' => __( 'Billing email', 'woonoow' ),
|
||
];
|
||
|
||
// Address fields only required for physical products
|
||
if ( $has_physical_product ) {
|
||
$required_billing_fields['address_1'] = __( 'Billing address', 'woonoow' );
|
||
$required_billing_fields['city'] = __( 'Billing city', 'woonoow' );
|
||
$required_billing_fields['postcode'] = __( 'Billing postcode', 'woonoow' );
|
||
$required_billing_fields['country'] = __( 'Billing country', 'woonoow' );
|
||
}
|
||
|
||
foreach ( $required_billing_fields as $field => $label ) {
|
||
if ( empty( $billing[ $field ] ) ) {
|
||
/* translators: %s: field label */
|
||
$validation_errors[] = sprintf( __( '%s is required', 'woonoow' ), $label );
|
||
}
|
||
}
|
||
|
||
// 3. Validate email format if provided
|
||
if ( ! empty( $billing['email'] ) && ! is_email( $billing['email'] ) ) {
|
||
$validation_errors[] = __( 'Billing email is not valid', 'woonoow' );
|
||
}
|
||
|
||
// If there are validation errors, return them
|
||
if ( ! empty( $validation_errors ) ) {
|
||
return new \WP_REST_Response( [
|
||
'error' => 'validation_failed',
|
||
'message' => __( 'Please complete all required fields', 'woonoow' ),
|
||
'fields' => $validation_errors
|
||
], 400 );
|
||
}
|
||
|
||
try {
|
||
$order = wc_create_order();
|
||
|
||
// Items
|
||
foreach ( $items as $row ) {
|
||
$pid = absint( $row['product_id'] ?? 0 );
|
||
$qty = max( 1, absint( $row['qty'] ?? 1 ) );
|
||
if ( ! $pid ) { continue; }
|
||
$product = wc_get_product( $pid );
|
||
if ( ! $product ) { continue; }
|
||
$order->add_product( $product, $qty );
|
||
}
|
||
|
||
// Billing
|
||
if ( $billing ) {
|
||
$order->set_address( [
|
||
'first_name' => (string) ( $billing['first_name'] ?? '' ),
|
||
'last_name' => (string) ( $billing['last_name'] ?? '' ),
|
||
'email' => (string) ( $billing['email'] ?? '' ),
|
||
'phone' => (string) ( $billing['phone'] ?? '' ),
|
||
'address_1' => (string) ( $billing['address_1'] ?? '' ),
|
||
'address_2' => (string) ( $billing['address_2'] ?? '' ),
|
||
'city' => (string) ( $billing['city'] ?? '' ),
|
||
'state' => (string) ( $billing['state'] ?? '' ),
|
||
'postcode' => (string) ( $billing['postcode'] ?? '' ),
|
||
'country' => (string) ( $billing['country'] ?? '' ),
|
||
], 'billing' );
|
||
}
|
||
|
||
// Shipping: copy billing unless explicit ship_to_different
|
||
$ship_to_different = (bool) ( $shipping['ship_to_different'] ?? false );
|
||
$ship_addr = $ship_to_different ? $shipping : $billing;
|
||
if ( $ship_addr ) {
|
||
$order->set_address( [
|
||
'first_name' => (string) ( $ship_addr['first_name'] ?? ( $ship_addr['name'] ?? '' ) ),
|
||
'last_name' => (string) ( $ship_addr['last_name'] ?? '' ),
|
||
'phone' => (string) ( $ship_addr['phone'] ?? '' ),
|
||
'address_1' => (string) ( $ship_addr['address_1'] ?? '' ),
|
||
'address_2' => (string) ( $ship_addr['address_2'] ?? '' ),
|
||
'city' => (string) ( $ship_addr['city'] ?? '' ),
|
||
'state' => (string) ( $ship_addr['state'] ?? '' ),
|
||
'postcode' => (string) ( $ship_addr['postcode'] ?? '' ),
|
||
'country' => (string) ( $ship_addr['country'] ?? '' ),
|
||
], 'shipping' );
|
||
}
|
||
|
||
// Shipping method - calculate using WooCommerce cart for accurate rates
|
||
if ( $shipping_method ) {
|
||
// Initialize cart if needed
|
||
if ( ! WC()->cart ) {
|
||
wc_load_cart();
|
||
}
|
||
if ( ! WC()->session ) {
|
||
WC()->session = new \WC_Session_Handler();
|
||
WC()->session->init();
|
||
}
|
||
|
||
// Temporarily use cart to calculate shipping
|
||
WC()->cart->empty_cart();
|
||
|
||
// Add items to cart
|
||
foreach ( $items as $row ) {
|
||
$pid = absint( $row['product_id'] ?? 0 );
|
||
$qty = max( 1, absint( $row['qty'] ?? 1 ) );
|
||
if ( $pid ) {
|
||
WC()->cart->add_to_cart( $pid, $qty );
|
||
}
|
||
}
|
||
|
||
// Set shipping address for calculation
|
||
if ( $ship_addr ) {
|
||
WC()->customer->set_shipping_country( $ship_addr['country'] ?? '' );
|
||
WC()->customer->set_shipping_state( $ship_addr['state'] ?? '' );
|
||
WC()->customer->set_shipping_postcode( $ship_addr['postcode'] ?? '' );
|
||
WC()->customer->set_shipping_city( $ship_addr['city'] ?? '' );
|
||
WC()->customer->set_shipping_address( $ship_addr['address_1'] ?? '' );
|
||
}
|
||
|
||
// Set chosen shipping method
|
||
WC()->session->set( 'chosen_shipping_methods', [ $shipping_method ] );
|
||
|
||
// Calculate shipping
|
||
WC()->cart->calculate_shipping();
|
||
WC()->cart->calculate_totals();
|
||
|
||
// Get the calculated rate
|
||
$packages = WC()->shipping()->get_packages();
|
||
$shipping_cost = 0;
|
||
$shipping_title = $shipping_method;
|
||
$shipping_taxes = [];
|
||
|
||
foreach ( $packages as $package ) {
|
||
if ( isset( $package['rates'][ $shipping_method ] ) ) {
|
||
$rate = $package['rates'][ $shipping_method ];
|
||
$shipping_cost = $rate->get_cost();
|
||
$shipping_title = $rate->get_label();
|
||
$shipping_taxes = $rate->get_taxes();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Clean up cart
|
||
WC()->cart->empty_cart();
|
||
|
||
// Parse method ID and instance ID
|
||
$method_id = $shipping_method;
|
||
$instance_id = null;
|
||
if ( strpos( $shipping_method, ':' ) !== false ) {
|
||
list( $method_id, $instance_str ) = explode( ':', $shipping_method, 2 );
|
||
$instance_id = absint( $instance_str );
|
||
}
|
||
|
||
// Add shipping line item
|
||
$ship_item = new \WC_Order_Item_Shipping();
|
||
$ship_item->set_method_id( $method_id );
|
||
if ( null !== $instance_id && method_exists( $ship_item, 'set_instance_id' ) ) {
|
||
$ship_item->set_instance_id( $instance_id );
|
||
}
|
||
$ship_item->set_method_title( $shipping_title );
|
||
$ship_item->set_total( $shipping_cost );
|
||
|
||
// Set shipping taxes if available
|
||
if ( ! empty( $shipping_taxes ) ) {
|
||
$ship_item->set_taxes( [ 'total' => $shipping_taxes ] );
|
||
}
|
||
|
||
$order->add_item( $ship_item );
|
||
}
|
||
|
||
// Payment method
|
||
if ( $payment_method ) {
|
||
$order->set_payment_method( $payment_method );
|
||
}
|
||
|
||
// Coupons
|
||
foreach ( $coupons as $code ) {
|
||
try {
|
||
$coupon = new \WC_Coupon( $code );
|
||
if ( $coupon && $coupon->get_code() ) {
|
||
// Apply coupon to order (this will calculate discount)
|
||
$order->apply_coupon( $coupon );
|
||
}
|
||
} catch ( \Throwable $e ) {
|
||
error_log( '[WooNooW] Coupon error: ' . $e->getMessage() );
|
||
}
|
||
}
|
||
|
||
// Customer note (visible to customer)
|
||
if ( $note ) {
|
||
$order->set_customer_note( $note );
|
||
}
|
||
|
||
// Calculate totals (includes products, shipping, coupons, taxes)
|
||
$order->calculate_totals( true );
|
||
|
||
// Set status after totals
|
||
try { $order->set_status( $status, 'Set via API' ); } catch ( \Throwable $e ) { /* ignore invalid */ }
|
||
|
||
// Block WooCommerce analytics tracking during save (same as update method)
|
||
add_filter( 'pre_http_request', function( $preempt, $args, $url ) {
|
||
if ( strpos( $url, 'pixel.wp.com' ) !== false || strpos( $url, 'stats.wp.com' ) !== false ) {
|
||
return new \WP_Error( 'http_request_blocked', 'WooCommerce analytics blocked during order save' );
|
||
}
|
||
return $preempt;
|
||
}, PHP_INT_MAX, 3 );
|
||
|
||
$order->save();
|
||
|
||
// Clean up
|
||
remove_all_filters( 'pre_http_request' );
|
||
|
||
// Process payment gateway if payment method is set and order is pending
|
||
if ( $payment_method && $status === 'pending' ) {
|
||
self::process_payment_gateway( $order, $payment_method );
|
||
}
|
||
|
||
// Link order to customer and update/create user account
|
||
if ( ! empty( $billing['email'] ) ) {
|
||
$email = sanitize_email( $billing['email'] );
|
||
|
||
// Check if user already exists
|
||
if ( email_exists( $email ) || username_exists( $email ) ) {
|
||
// User exists - always link and update data
|
||
$user = get_user_by( 'email', $email );
|
||
if ( $user ) {
|
||
// Upgrade subscriber to customer role (WooCommerce best practice)
|
||
if ( in_array( 'subscriber', (array) $user->roles, true ) ) {
|
||
$user->set_role( 'customer' );
|
||
}
|
||
|
||
// Update customer billing & shipping data
|
||
$customer = new \WC_Customer( $user->ID );
|
||
|
||
// Update billing address
|
||
if ( ! empty( $billing['first_name'] ) ) $customer->set_billing_first_name( $billing['first_name'] );
|
||
if ( ! empty( $billing['last_name'] ) ) $customer->set_billing_last_name( $billing['last_name'] );
|
||
if ( ! empty( $billing['email'] ) ) $customer->set_billing_email( $billing['email'] );
|
||
if ( ! empty( $billing['phone'] ) ) $customer->set_billing_phone( $billing['phone'] );
|
||
if ( ! empty( $billing['address_1'] ) ) $customer->set_billing_address_1( $billing['address_1'] );
|
||
if ( ! empty( $billing['city'] ) ) $customer->set_billing_city( $billing['city'] );
|
||
if ( ! empty( $billing['state'] ) ) $customer->set_billing_state( $billing['state'] );
|
||
if ( ! empty( $billing['postcode'] ) ) $customer->set_billing_postcode( $billing['postcode'] );
|
||
if ( ! empty( $billing['country'] ) ) $customer->set_billing_country( $billing['country'] );
|
||
|
||
// Update shipping address (if provided)
|
||
if ( ! empty( $shipping ) && is_array( $shipping ) ) {
|
||
if ( ! empty( $shipping['first_name'] ) ) $customer->set_shipping_first_name( $shipping['first_name'] );
|
||
if ( ! empty( $shipping['last_name'] ) ) $customer->set_shipping_last_name( $shipping['last_name'] );
|
||
if ( ! empty( $shipping['address_1'] ) ) $customer->set_shipping_address_1( $shipping['address_1'] );
|
||
if ( ! empty( $shipping['city'] ) ) $customer->set_shipping_city( $shipping['city'] );
|
||
if ( ! empty( $shipping['state'] ) ) $customer->set_shipping_state( $shipping['state'] );
|
||
if ( ! empty( $shipping['postcode'] ) ) $customer->set_shipping_postcode( $shipping['postcode'] );
|
||
if ( ! empty( $shipping['country'] ) ) $customer->set_shipping_country( $shipping['country'] );
|
||
}
|
||
|
||
$customer->save();
|
||
|
||
$order->set_customer_id( $user->ID );
|
||
$order->save();
|
||
}
|
||
} elseif ( $register_member ) {
|
||
// User doesn't exist - create only if checkbox is checked
|
||
// Generate password
|
||
$password = wp_generate_password( 12, true, true );
|
||
|
||
// Prepare user data
|
||
$userdata = [
|
||
'user_login' => $email,
|
||
'user_email' => $email,
|
||
'user_pass' => $password,
|
||
'first_name' => $billing['first_name'] ?? '',
|
||
'last_name' => $billing['last_name'] ?? '',
|
||
'display_name' => trim( ( $billing['first_name'] ?? '' ) . ' ' . ( $billing['last_name'] ?? '' ) ) ?: $email,
|
||
'role' => 'customer', // WooCommerce customer role
|
||
];
|
||
|
||
$user_id = wp_insert_user( $userdata );
|
||
|
||
if ( ! is_wp_error( $user_id ) ) {
|
||
// Set WooCommerce customer billing data
|
||
$customer = new \WC_Customer( $user_id );
|
||
|
||
// Billing address
|
||
if ( ! empty( $billing['first_name'] ) ) $customer->set_billing_first_name( $billing['first_name'] );
|
||
if ( ! empty( $billing['last_name'] ) ) $customer->set_billing_last_name( $billing['last_name'] );
|
||
if ( ! empty( $billing['email'] ) ) $customer->set_billing_email( $billing['email'] );
|
||
if ( ! empty( $billing['phone'] ) ) $customer->set_billing_phone( $billing['phone'] );
|
||
if ( ! empty( $billing['address_1'] ) ) $customer->set_billing_address_1( $billing['address_1'] );
|
||
if ( ! empty( $billing['city'] ) ) $customer->set_billing_city( $billing['city'] );
|
||
if ( ! empty( $billing['state'] ) ) $customer->set_billing_state( $billing['state'] );
|
||
if ( ! empty( $billing['postcode'] ) ) $customer->set_billing_postcode( $billing['postcode'] );
|
||
if ( ! empty( $billing['country'] ) ) $customer->set_billing_country( $billing['country'] );
|
||
|
||
// Shipping address (if provided)
|
||
if ( ! empty( $shipping ) && is_array( $shipping ) ) {
|
||
if ( ! empty( $shipping['first_name'] ) ) $customer->set_shipping_first_name( $shipping['first_name'] );
|
||
if ( ! empty( $shipping['last_name'] ) ) $customer->set_shipping_last_name( $shipping['last_name'] );
|
||
if ( ! empty( $shipping['address_1'] ) ) $customer->set_shipping_address_1( $shipping['address_1'] );
|
||
if ( ! empty( $shipping['city'] ) ) $customer->set_shipping_city( $shipping['city'] );
|
||
if ( ! empty( $shipping['state'] ) ) $customer->set_shipping_state( $shipping['state'] );
|
||
if ( ! empty( $shipping['postcode'] ) ) $customer->set_shipping_postcode( $shipping['postcode'] );
|
||
if ( ! empty( $shipping['country'] ) ) $customer->set_shipping_country( $shipping['country'] );
|
||
}
|
||
|
||
$customer->save();
|
||
|
||
// Link order to customer
|
||
$order->set_customer_id( $user_id );
|
||
$order->save();
|
||
|
||
// Send new user notification (password reset link)
|
||
wp_new_user_notification( $user_id, null, 'user' );
|
||
}
|
||
}
|
||
}
|
||
|
||
// Schedule email notification with 15s delay (non-blocking) - on shutdown to avoid blocking
|
||
add_action( 'shutdown', function() use ( $order, $status ) {
|
||
self::schedule_order_email( $order->get_id(), $status );
|
||
}, 999 );
|
||
|
||
return new \WP_REST_Response( [
|
||
'ok' => true,
|
||
'id' => $order->get_id(),
|
||
'number' => $order->get_order_number(),
|
||
], 201 );
|
||
|
||
} catch ( \Throwable $e ) {
|
||
// Log the actual error for debugging
|
||
error_log('[WooNooW] Order creation failed: ' . $e->getMessage());
|
||
|
||
// Return user-friendly error message
|
||
return new \WP_REST_Response( [
|
||
'error' => 'create_failed',
|
||
'message' => __( 'Unable to create order. Please check all required fields and try again.', 'woonoow' )
|
||
], 500 );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET /woonoow/v1/products
|
||
*/
|
||
public static function products( WP_REST_Request $req ) : WP_REST_Response {
|
||
$s = sanitize_text_field( $req->get_param('search') ?? '' );
|
||
$limit = max( 1, min( 20, absint( $req->get_param('limit') ?? 10 ) ) );
|
||
|
||
$args = [ 'limit' => $limit, 'status' => 'publish' ];
|
||
if ( $s ) { $args['s'] = $s; }
|
||
|
||
$prods = wc_get_products( $args );
|
||
$rows = array_map( function( $p ) {
|
||
return [
|
||
'id' => $p->get_id(),
|
||
'name' => $p->get_name(),
|
||
'price' => (float) $p->get_price(),
|
||
'regular_price' => (float) $p->get_regular_price(),
|
||
'sale_price' => $p->get_sale_price() ? (float) $p->get_sale_price() : null,
|
||
'sku' => $p->get_sku(),
|
||
'stock' => $p->get_stock_quantity(),
|
||
'virtual' => $p->is_virtual(),
|
||
'downloadable' => $p->is_downloadable(),
|
||
];
|
||
}, $prods );
|
||
|
||
return new WP_REST_Response( [ 'rows' => $rows ], 200 );
|
||
}
|
||
|
||
public static function payments( WP_REST_Request $req ) : WP_REST_Response {
|
||
$methods = [];
|
||
if ( function_exists('WC') ) {
|
||
$gateways = WC()->payment_gateways ? WC()->payment_gateways->get_available_payment_gateways() : [];
|
||
|
||
foreach ( $gateways as $id => $gw ) {
|
||
// Skip disabled gateways (double-check even though get_available_payment_gateways should filter)
|
||
if ( ! isset( $gw->enabled ) || $gw->enabled !== 'yes' ) {
|
||
continue;
|
||
}
|
||
|
||
$gateway_data = [
|
||
'id' => (string) $id,
|
||
'title' => (string) ( $gw->get_title() ?? $gw->title ?? $id ),
|
||
'enabled' => true,
|
||
'channels' => [],
|
||
];
|
||
|
||
/**
|
||
* Filter: woonoow/payment_gateway_channels
|
||
*
|
||
* Allows payment gateways to provide channel-level options (e.g., bank accounts).
|
||
* If channels are provided, they will be shown instead of the gateway itself.
|
||
*
|
||
* @param array $channels Array of channel configurations
|
||
* @param string $gateway_id Gateway ID
|
||
* @param object $gateway Gateway instance
|
||
*
|
||
* Example:
|
||
* add_filter('woonoow/payment_gateway_channels', function($channels, $gateway_id, $gateway) {
|
||
* if ($gateway_id === 'bacs') {
|
||
* $accounts = $gateway->get_option('account_details', []);
|
||
* foreach ($accounts as $account) {
|
||
* $channels[] = [
|
||
* 'id' => 'bacs_' . sanitize_title($account['account_name']),
|
||
* 'title' => $account['bank_name'] . ' - ' . $account['account_name'],
|
||
* 'meta' => $account,
|
||
* ];
|
||
* }
|
||
* }
|
||
* return $channels;
|
||
* }, 10, 3);
|
||
*/
|
||
$channels = apply_filters( 'woonoow/payment_gateway_channels', [], $id, $gw );
|
||
|
||
// If gateway has channels, add them
|
||
if ( ! empty( $channels ) && is_array( $channels ) ) {
|
||
$gateway_data['channels'] = $channels;
|
||
}
|
||
|
||
$methods[] = $gateway_data;
|
||
}
|
||
}
|
||
return new \WP_REST_Response( $methods, 200 );
|
||
}
|
||
|
||
public static function shippings( WP_REST_Request $req ) : WP_REST_Response {
|
||
$rows = [];
|
||
if ( ! class_exists( '\WC_Shipping_Zones' ) ) {
|
||
return new \WP_REST_Response( $rows, 200 );
|
||
}
|
||
|
||
$zones = \WC_Shipping_Zones::get_zones();
|
||
// Also include the “locations not covered by your other zones” pseudo-zone
|
||
$zones[] = \WC_Shipping_Zones::get_zone_by( 'zone_id', 0 )->get_data();
|
||
|
||
foreach ( $zones as $zone ) {
|
||
$zone_obj = is_object( $zone ) ? $zone : new \WC_Shipping_Zone( $zone['id'] ?? 0 );
|
||
foreach ( $zone_obj->get_shipping_methods() as $m ) {
|
||
/** @var \WC_Shipping_Method $m */
|
||
|
||
// Skip disabled shipping methods
|
||
if ( ! $m->is_enabled() ) {
|
||
continue;
|
||
}
|
||
|
||
$id = $m->id;
|
||
$instance = method_exists( $m, 'get_instance_id' ) ? $m->get_instance_id() : 0;
|
||
$title = !empty($m->title) ? $m->title :
|
||
(method_exists($m, 'get_method_title') ? $m->get_method_title() : $m->id);
|
||
|
||
// Get shipping cost
|
||
$cost = 0;
|
||
if ( method_exists( $m, 'get_option' ) ) {
|
||
$cost = $m->get_option( 'cost', 0 );
|
||
}
|
||
|
||
$rows[] = [
|
||
'id' => $instance ? "{$id}:{$instance}" : $id, // e.g. "flat_rate:3" or "free_shipping"
|
||
'method' => $id,
|
||
'title' => (string) ( $m->title ?? $m->get_method_title() ?? $id ), // Use user's label first
|
||
'cost' => (float) $cost, // Shipping cost
|
||
];
|
||
}
|
||
}
|
||
// Deduplicate by id
|
||
$rows = array_values( array_reduce( $rows, function($carry, $r){
|
||
$carry[$r['id']] = $r; return $carry;
|
||
}, [] ) );
|
||
|
||
return new \WP_REST_Response( $rows, 200 );
|
||
}
|
||
|
||
/**
|
||
* Calculate shipping rates for given cart and address
|
||
* POST /woonoow/v1/shipping/calculate
|
||
*/
|
||
public static function calculate_shipping( WP_REST_Request $req ) : \WP_REST_Response {
|
||
$p = $req->get_json_params();
|
||
$items = is_array( $p['items'] ?? null ) ? $p['items'] : [];
|
||
$shipping = is_array( $p['shipping'] ?? null ) ? $p['shipping'] : [];
|
||
|
||
if ( empty( $items ) ) {
|
||
return new \WP_REST_Response( [ 'error' => 'items_required' ], 400 );
|
||
}
|
||
|
||
try {
|
||
// Initialize WooCommerce cart and session if not already
|
||
if ( ! WC()->cart ) {
|
||
wc_load_cart();
|
||
}
|
||
if ( ! WC()->session ) {
|
||
WC()->session = new \WC_Session_Handler();
|
||
WC()->session->init();
|
||
}
|
||
|
||
// Create a temporary cart to calculate shipping
|
||
WC()->cart->empty_cart();
|
||
|
||
// Add items to cart
|
||
foreach ( $items as $item ) {
|
||
$product_id = absint( $item['product_id'] ?? 0 );
|
||
$qty = max( 1, absint( $item['qty'] ?? 1 ) );
|
||
if ( $product_id ) {
|
||
WC()->cart->add_to_cart( $product_id, $qty );
|
||
}
|
||
}
|
||
|
||
// Set customer shipping address
|
||
if ( ! empty( $shipping ) ) {
|
||
$country = $shipping['country'] ?? '';
|
||
$state = $shipping['state'] ?? '';
|
||
$postcode = $shipping['postcode'] ?? '';
|
||
$city = $shipping['city'] ?? '';
|
||
$address_1 = $shipping['address_1'] ?? '';
|
||
$address_2 = $shipping['address_2'] ?? '';
|
||
|
||
WC()->customer->set_shipping_country( $country );
|
||
WC()->customer->set_shipping_state( $state );
|
||
WC()->customer->set_shipping_postcode( $postcode );
|
||
WC()->customer->set_shipping_city( $city );
|
||
WC()->customer->set_shipping_address( $address_1 );
|
||
WC()->customer->set_shipping_address_2( $address_2 );
|
||
|
||
// Also set billing for tax calculation context
|
||
WC()->customer->set_billing_country( $country );
|
||
WC()->customer->set_billing_state( $state );
|
||
WC()->customer->set_billing_postcode( $postcode );
|
||
WC()->customer->set_billing_city( $city );
|
||
|
||
// Support for Rajaongkir plugin - set destination in session
|
||
// Rajaongkir uses session-based destination instead of standard address fields
|
||
if ( $country === 'ID' && ! empty( $shipping['destination_id'] ) ) {
|
||
WC()->session->set( 'selected_destination_id', $shipping['destination_id'] );
|
||
WC()->session->set( 'selected_destination_label', $shipping['destination_label'] ?? $city );
|
||
} else {
|
||
// Clear Rajaongkir session data for non-ID countries
|
||
WC()->session->__unset( 'selected_destination_id' );
|
||
WC()->session->__unset( 'selected_destination_label' );
|
||
}
|
||
}
|
||
|
||
// Calculate shipping
|
||
WC()->cart->calculate_shipping();
|
||
WC()->cart->calculate_totals();
|
||
|
||
// Get available shipping packages and rates
|
||
$packages = WC()->shipping()->get_packages();
|
||
$methods = [];
|
||
|
||
foreach ( $packages as $package_key => $package ) {
|
||
$rates = $package['rates'] ?? [];
|
||
|
||
foreach ( $rates as $rate_id => $rate ) {
|
||
/** @var \WC_Shipping_Rate $rate */
|
||
$methods[] = [
|
||
'id' => $rate_id,
|
||
'method_id' => $rate->get_method_id(),
|
||
'instance_id' => $rate->get_instance_id(),
|
||
'label' => $rate->get_label(),
|
||
'cost' => (float) $rate->get_cost(),
|
||
'taxes' => $rate->get_taxes(),
|
||
'meta_data' => $rate->get_meta_data(),
|
||
];
|
||
}
|
||
}
|
||
|
||
// Clean up
|
||
WC()->cart->empty_cart();
|
||
|
||
return new \WP_REST_Response( [
|
||
'methods' => $methods,
|
||
'has_methods' => ! empty( $methods ),
|
||
], 200 );
|
||
|
||
} catch ( \Throwable $e ) {
|
||
// Clean up on error
|
||
WC()->cart->empty_cart();
|
||
|
||
return new \WP_REST_Response( [
|
||
'error' => 'calculation_failed',
|
||
'message' => $e->getMessage(),
|
||
], 500 );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Preview order totals (subtotal, shipping, tax, total)
|
||
* POST /woonoow/v1/orders/preview
|
||
*/
|
||
public static function preview_order( WP_REST_Request $req ) : \WP_REST_Response {
|
||
$p = $req->get_json_params();
|
||
$items = is_array( $p['items'] ?? null ) ? $p['items'] : [];
|
||
$billing = is_array( $p['billing'] ?? null ) ? $p['billing'] : [];
|
||
$shipping = is_array( $p['shipping'] ?? null ) ? $p['shipping'] : [];
|
||
$shipping_method = sanitize_text_field( $p['shipping_method'] ?? '' );
|
||
$coupons = array_filter( array_map( 'sanitize_text_field', (array) ( $p['coupons'] ?? [] ) ) );
|
||
|
||
if ( empty( $items ) ) {
|
||
return new \WP_REST_Response( [ 'error' => 'items_required' ], 400 );
|
||
}
|
||
|
||
try {
|
||
// Initialize WooCommerce cart and session if not already
|
||
if ( ! WC()->cart ) {
|
||
wc_load_cart();
|
||
}
|
||
if ( ! WC()->session ) {
|
||
WC()->session = new \WC_Session_Handler();
|
||
WC()->session->init();
|
||
}
|
||
|
||
// Use WooCommerce cart for calculation
|
||
WC()->cart->empty_cart();
|
||
|
||
// Add items
|
||
foreach ( $items as $item ) {
|
||
$product_id = absint( $item['product_id'] ?? 0 );
|
||
$qty = max( 1, absint( $item['qty'] ?? 1 ) );
|
||
if ( $product_id ) {
|
||
WC()->cart->add_to_cart( $product_id, $qty );
|
||
}
|
||
}
|
||
|
||
// Set customer addresses for tax calculation
|
||
if ( ! empty( $billing ) ) {
|
||
WC()->customer->set_billing_country( $billing['country'] ?? '' );
|
||
WC()->customer->set_billing_state( $billing['state'] ?? '' );
|
||
WC()->customer->set_billing_postcode( $billing['postcode'] ?? '' );
|
||
WC()->customer->set_billing_city( $billing['city'] ?? '' );
|
||
}
|
||
|
||
if ( ! empty( $shipping ) ) {
|
||
WC()->customer->set_shipping_country( $shipping['country'] ?? '' );
|
||
WC()->customer->set_shipping_state( $shipping['state'] ?? '' );
|
||
WC()->customer->set_shipping_postcode( $shipping['postcode'] ?? '' );
|
||
WC()->customer->set_shipping_city( $shipping['city'] ?? '' );
|
||
}
|
||
|
||
// Apply coupons
|
||
foreach ( $coupons as $code ) {
|
||
WC()->cart->apply_coupon( $code );
|
||
}
|
||
|
||
// Set chosen shipping method if provided
|
||
if ( $shipping_method ) {
|
||
WC()->session->set( 'chosen_shipping_methods', [ $shipping_method ] );
|
||
}
|
||
|
||
// Calculate totals
|
||
WC()->cart->calculate_shipping();
|
||
WC()->cart->calculate_totals();
|
||
|
||
// Get totals
|
||
$result = [
|
||
'subtotal' => (float) WC()->cart->get_subtotal(),
|
||
'subtotal_tax' => (float) WC()->cart->get_subtotal_tax(),
|
||
'discount_total' => (float) WC()->cart->get_discount_total(),
|
||
'discount_tax' => (float) WC()->cart->get_discount_tax(),
|
||
'shipping_total' => (float) WC()->cart->get_shipping_total(),
|
||
'shipping_tax' => (float) WC()->cart->get_shipping_tax(),
|
||
'cart_contents_tax' => (float) WC()->cart->get_cart_contents_tax(),
|
||
'fee_tax' => (float) WC()->cart->get_fee_tax(),
|
||
'total_tax' => (float) WC()->cart->get_total_tax(),
|
||
'total' => (float) WC()->cart->get_total( 'edit' ),
|
||
'tax_display_cart' => get_option( 'woocommerce_tax_display_cart', 'excl' ),
|
||
'prices_include_tax' => wc_prices_include_tax(),
|
||
];
|
||
|
||
// Clean up
|
||
WC()->cart->empty_cart();
|
||
|
||
return new \WP_REST_Response( $result, 200 );
|
||
|
||
} catch ( \Throwable $e ) {
|
||
// Clean up on error
|
||
WC()->cart->empty_cart();
|
||
|
||
return new \WP_REST_Response( [
|
||
'error' => 'preview_failed',
|
||
'message' => $e->getMessage(),
|
||
], 500 );
|
||
}
|
||
}
|
||
|
||
public static function countries( WP_REST_Request $req ) : \WP_REST_Response {
|
||
$rows = [];
|
||
$states = [];
|
||
$default = '';
|
||
|
||
if ( function_exists('wc') && wc() && isset( wc()->countries ) ) {
|
||
$c = wc()->countries;
|
||
|
||
// All countries and which ones are allowed for checkout
|
||
$all = (array) $c->get_countries(); // [ 'ID' => 'Indonesia', ... ]
|
||
$allowed = (array) $c->get_allowed_countries(); // obeys General ▶ Selling location(s)
|
||
$states = (array) $c->get_states(); // [ 'US' => [ 'CA' => 'California', ... ], ... ]
|
||
|
||
// Use only the allowed countries list; if empty, fall back to all.
|
||
$use = ! empty( $allowed ) ? $allowed : $all; // shape: [ 'ID' => 'Indonesia', ... ]
|
||
|
||
// Default country is the store base if present in the allowed set; otherwise first allowed.
|
||
$base = wc_get_base_location(); // ['country' => 'ID', 'state' => 'JK']
|
||
$default = isset( $base['country'] ) && array_key_exists( $base['country'], $use )
|
||
? $base['country']
|
||
: (string) ( array_key_first( $use ) ?: '' );
|
||
|
||
// Filter states down to the allowed countries only
|
||
if ( ! empty( $states ) ) {
|
||
$states = array_intersect_key( $states, $use );
|
||
}
|
||
|
||
foreach ( $use as $code => $name ) {
|
||
$rows[] = [
|
||
'code' => (string) $code,
|
||
'name' => (string) $name,
|
||
];
|
||
}
|
||
}
|
||
|
||
// Shape: { countries: [{code,name}], states: { CODE: {STATE: Name} }, default_country: 'ID' }
|
||
return new \WP_REST_Response([
|
||
'countries' => $rows,
|
||
'states' => $states,
|
||
'default_country' => $default,
|
||
], 200 );
|
||
}
|
||
|
||
/**
|
||
* Send order email notification (runs async via Action Scheduler or wp-cron).
|
||
* This is triggered 15 seconds after order status change to avoid blocking API responses.
|
||
*/
|
||
public static function send_order_email( $args ) {
|
||
$order_id = absint( $args['order_id'] ?? 0 );
|
||
$status = sanitize_text_field( $args['status'] ?? '' );
|
||
|
||
if ( ! $order_id || ! $status ) {
|
||
return;
|
||
}
|
||
|
||
$order = wc_get_order( $order_id );
|
||
if ( ! $order ) {
|
||
return;
|
||
}
|
||
|
||
// Trigger WooCommerce email notification for the status
|
||
// This will call wp_mail() which is intercepted by WooEmailOverride
|
||
do_action( 'woocommerce_order_status_' . $status, $order_id, $order );
|
||
|
||
// Also trigger generic status change hook
|
||
do_action( 'woocommerce_order_status_changed', $order_id, $order->get_status(), $status, $order );
|
||
}
|
||
|
||
/**
|
||
* Delete an order (move to trash or permanently delete based on WooCommerce settings)
|
||
*/
|
||
public static function delete( WP_REST_Request $req ) : WP_REST_Response {
|
||
$id = (int) $req['id'];
|
||
|
||
if ( ! $id ) {
|
||
return new WP_REST_Response([
|
||
'error' => __( 'Invalid order ID', 'woonoow' ),
|
||
], 400 );
|
||
}
|
||
|
||
$order = wc_get_order( $id );
|
||
if ( ! $order ) {
|
||
return new WP_REST_Response([
|
||
'error' => __( 'Order not found', 'woonoow' ),
|
||
], 404 );
|
||
}
|
||
|
||
// Check if HPOS is enabled
|
||
$using_hpos = OrderUtil::custom_orders_table_usage_is_enabled();
|
||
|
||
try {
|
||
// For HPOS orders, use the delete method directly
|
||
if ( $using_hpos ) {
|
||
// Move to trash first (soft delete)
|
||
$order->delete( false );
|
||
|
||
// If you want to permanently delete instead, use:
|
||
// $order->delete( true );
|
||
} else {
|
||
// For legacy post-based orders, use wp_trash_post or wp_delete_post
|
||
wp_trash_post( $id );
|
||
|
||
// For permanent deletion:
|
||
// wp_delete_post( $id, true );
|
||
}
|
||
|
||
// Log the deletion
|
||
if ( function_exists( 'wc_get_logger' ) ) {
|
||
$logger = wc_get_logger();
|
||
$logger->info(
|
||
sprintf( 'Order #%d deleted via WooNooW API by user %d', $id, get_current_user_id() ),
|
||
[ 'source' => 'woonoow-orders' ]
|
||
);
|
||
}
|
||
|
||
return new WP_REST_Response([
|
||
'success' => true,
|
||
'id' => $id,
|
||
'message' => __( 'Order deleted successfully', 'woonoow' ),
|
||
], 200 );
|
||
|
||
} catch ( \Exception $e ) {
|
||
return new WP_REST_Response([
|
||
'error' => __( 'Failed to delete order', 'woonoow' ),
|
||
'message' => $e->getMessage(),
|
||
], 500 );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get payment method title from order
|
||
* Handles both regular gateways and channel-based payments (e.g., bacs_dwindi-ramadhana_0)
|
||
*
|
||
* @param \WC_Order $order Order object
|
||
* @return string Payment method title
|
||
*/
|
||
private static function get_payment_method_title( $order ): string {
|
||
$payment_method_id = $order->get_payment_method();
|
||
|
||
if ( empty( $payment_method_id ) ) {
|
||
return __('No payment method', 'woonoow');
|
||
}
|
||
|
||
// Get ALL payment gateways (not just available ones)
|
||
if ( function_exists('WC') && WC()->payment_gateways ) {
|
||
$all_gateways = WC()->payment_gateways->payment_gateways();
|
||
|
||
// First, check if it's a direct gateway ID (e.g., tripay_bniva, bacs, cod)
|
||
if ( isset( $all_gateways[ $payment_method_id ] ) ) {
|
||
$gateway = $all_gateways[ $payment_method_id ];
|
||
|
||
// Check if this gateway has channels
|
||
$channels = apply_filters( 'woonoow/payment_gateway_channels', [], $payment_method_id, $gateway );
|
||
|
||
// If no channels, return gateway title
|
||
if ( empty( $channels ) ) {
|
||
return $gateway->get_title();
|
||
}
|
||
}
|
||
|
||
// If not a direct gateway, check if it's a channel ID (e.g., bacs_dwindi-ramadhana_0)
|
||
if ( strpos( $payment_method_id, '_' ) !== false ) {
|
||
// Extract gateway ID (e.g., "bacs" from "bacs_dwindi-ramadhana_0")
|
||
$parts = explode( '_', $payment_method_id, 2 );
|
||
$gateway_id = $parts[0];
|
||
|
||
if ( isset( $all_gateways[ $gateway_id ] ) ) {
|
||
$gateway = $all_gateways[ $gateway_id ];
|
||
|
||
// Get channels for this gateway
|
||
$channels = apply_filters( 'woonoow/payment_gateway_channels', [], $gateway_id, $gateway );
|
||
|
||
// Find matching channel
|
||
foreach ( $channels as $channel ) {
|
||
if ( $channel['id'] === $payment_method_id ) {
|
||
return $channel['title'];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback to WooCommerce's get_payment_method_title()
|
||
$title = $order->get_payment_method_title();
|
||
return $title ?: __('No payment method', 'woonoow');
|
||
}
|
||
|
||
/**
|
||
* Get shipping method title from order
|
||
* Uses WC_Shipping_Zones to get the actual user-configured title
|
||
*
|
||
* @param \WC_Order $order Order object
|
||
* @return string Shipping method title
|
||
*/
|
||
private static function get_shipping_method_title( $order ): string {
|
||
$shipping_methods = $order->get_shipping_methods();
|
||
|
||
if ( empty( $shipping_methods ) ) {
|
||
return __('No shipping method', 'woonoow');
|
||
}
|
||
|
||
// Get first shipping method item
|
||
$shipping_item = reset( $shipping_methods );
|
||
$method_id = $shipping_item->get_method_id();
|
||
$instance_id = $shipping_item->get_instance_id();
|
||
|
||
// Look up the actual shipping method from WC_Shipping_Zones
|
||
if ( class_exists( '\WC_Shipping_Zones' ) && $instance_id ) {
|
||
$zones = \WC_Shipping_Zones::get_zones();
|
||
$zones[] = \WC_Shipping_Zones::get_zone_by( 'zone_id', 0 )->get_data();
|
||
|
||
foreach ( $zones as $zone ) {
|
||
$zone_obj = is_object( $zone ) ? $zone : new \WC_Shipping_Zone( $zone['id'] ?? 0 );
|
||
foreach ( $zone_obj->get_shipping_methods() as $method ) {
|
||
// Match by method_id and instance_id
|
||
if ( $method->id === $method_id && $method->get_instance_id() == $instance_id ) {
|
||
// Found it! Return the user's custom title
|
||
return $method->title ?: $method->get_method_title() ?: $method_id;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback: try to get title from the order item itself
|
||
$title = $shipping_item->get_name();
|
||
if ( $title && $title !== $method_id ) {
|
||
return $title;
|
||
}
|
||
|
||
// Last resort: return method ID
|
||
return $method_id ?: __('No shipping method', 'woonoow');
|
||
}
|
||
|
||
/**
|
||
* Get shipping method ID from order
|
||
* WooCommerce format: "method_id:instance_id" (e.g., "free_shipping:1")
|
||
*
|
||
* @param \WC_Order $order Order object
|
||
* @return string Shipping method ID with instance
|
||
*/
|
||
private static function get_shipping_method_id( $order ): string {
|
||
$shipping_methods = $order->get_shipping_methods();
|
||
if ( empty( $shipping_methods ) ) {
|
||
return '';
|
||
}
|
||
|
||
// Get first shipping method
|
||
$shipping_method = reset( $shipping_methods );
|
||
$method_id = $shipping_method->get_method_id();
|
||
$instance_id = $shipping_method->get_instance_id();
|
||
|
||
// Return in WooCommerce format: method_id:instance_id
|
||
return $instance_id ? "{$method_id}:{$instance_id}" : $method_id;
|
||
}
|
||
|
||
/**
|
||
* GET /woonoow/v1/customers/search
|
||
* Search customers by email or general search for autofill functionality
|
||
*/
|
||
public static function search_customers( WP_REST_Request $req ) : WP_REST_Response {
|
||
$email = sanitize_email( $req->get_param( 'email' ) ?? '' );
|
||
$search = sanitize_text_field( $req->get_param( 'search' ) ?? '' );
|
||
|
||
// If email provided, search by email (exact match)
|
||
if ( ! empty( $email ) ) {
|
||
$user = get_user_by( 'email', $email );
|
||
|
||
if ( ! $user ) {
|
||
return new \WP_REST_Response( [ 'found' => false ], 200 );
|
||
}
|
||
|
||
return new \WP_REST_Response( self::format_customer_data( $user ), 200 );
|
||
}
|
||
|
||
// If search provided, search by name/email (multiple results)
|
||
if ( ! empty( $search ) ) {
|
||
$users = get_users( [
|
||
'search' => '*' . $search . '*',
|
||
'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
|
||
'number' => 10,
|
||
'role__in' => [ 'customer', 'administrator', 'shop_manager', 'subscriber' ],
|
||
] );
|
||
|
||
$results = [];
|
||
foreach ( $users as $user ) {
|
||
$customer = new \WC_Customer( $user->ID );
|
||
$results[] = [
|
||
'id' => $user->ID,
|
||
'email' => $user->user_email,
|
||
'name' => trim( ( $customer->get_billing_first_name() ?: $user->first_name ) . ' ' . ( $customer->get_billing_last_name() ?: $user->last_name ) ),
|
||
'first_name' => $customer->get_billing_first_name() ?: $user->first_name,
|
||
'last_name' => $customer->get_billing_last_name() ?: $user->last_name,
|
||
];
|
||
}
|
||
|
||
return new \WP_REST_Response( $results, 200 );
|
||
}
|
||
|
||
return new \WP_REST_Response( [ 'error' => 'email_or_search_required' ], 400 );
|
||
}
|
||
|
||
/**
|
||
* Format customer data for autofill
|
||
*/
|
||
private static function format_customer_data( $user ): array {
|
||
$customer = new \WC_Customer( $user->ID );
|
||
|
||
return [
|
||
'found' => true,
|
||
'user_id' => $user->ID,
|
||
'email' => $user->user_email,
|
||
'first_name' => $customer->get_billing_first_name() ?: $user->first_name,
|
||
'last_name' => $customer->get_billing_last_name() ?: $user->last_name,
|
||
'phone' => $customer->get_billing_phone(),
|
||
'billing' => [
|
||
'first_name' => $customer->get_billing_first_name(),
|
||
'last_name' => $customer->get_billing_last_name(),
|
||
'email' => $customer->get_billing_email() ?: $user->user_email,
|
||
'phone' => $customer->get_billing_phone(),
|
||
'address_1' => $customer->get_billing_address_1(),
|
||
'address_2' => $customer->get_billing_address_2(),
|
||
'city' => $customer->get_billing_city(),
|
||
'state' => $customer->get_billing_state(),
|
||
'postcode' => $customer->get_billing_postcode(),
|
||
'country' => $customer->get_billing_country(),
|
||
],
|
||
'shipping' => [
|
||
'first_name' => $customer->get_shipping_first_name(),
|
||
'last_name' => $customer->get_shipping_last_name(),
|
||
'address_1' => $customer->get_shipping_address_1(),
|
||
'address_2' => $customer->get_shipping_address_2(),
|
||
'city' => $customer->get_shipping_city(),
|
||
'state' => $customer->get_shipping_state(),
|
||
'postcode' => $customer->get_shipping_postcode(),
|
||
'country' => $customer->get_shipping_country(),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Process payment gateway for an order
|
||
* Calls the gateway's process_payment() method to create external transactions
|
||
*
|
||
* @param \WC_Order $order Order object
|
||
* @param string $payment_method Payment method ID
|
||
* @return array|\WP_Error Payment result or error
|
||
*/
|
||
private static function process_payment_gateway( $order, $payment_method ) {
|
||
// Get all payment gateways
|
||
if ( ! function_exists('WC') || ! WC()->payment_gateways ) {
|
||
return new \WP_Error( 'no_wc', __( 'WooCommerce not initialized', 'woonoow' ) );
|
||
}
|
||
|
||
// Initialize WooCommerce cart and session if not exists
|
||
// Some gateways call WC()->cart->empty_cart() or WC()->session->set() after payment processing
|
||
if ( ! WC()->cart ) {
|
||
WC()->initialize_cart();
|
||
}
|
||
|
||
// Initialize session if not exists
|
||
if ( ! WC()->session || ! WC()->session instanceof \WC_Session ) {
|
||
WC()->initialize_session();
|
||
}
|
||
|
||
$gateways = WC()->payment_gateways->payment_gateways();
|
||
|
||
// Extract base gateway ID (handle channels like bacs_account_0)
|
||
$gateway_id = $payment_method;
|
||
if ( strpos( $payment_method, '_' ) !== false ) {
|
||
$parts = explode( '_', $payment_method, 2 );
|
||
// Check if base gateway exists
|
||
if ( isset( $gateways[ $parts[0] ] ) ) {
|
||
$gateway_id = $parts[0];
|
||
}
|
||
}
|
||
|
||
// Check if gateway exists
|
||
if ( ! isset( $gateways[ $gateway_id ] ) ) {
|
||
error_log( '[WooNooW] Payment gateway not found: ' . $gateway_id );
|
||
return new \WP_Error( 'gateway_not_found', sprintf( __( 'Payment gateway not found: %s', 'woonoow' ), $gateway_id ) );
|
||
}
|
||
|
||
$gateway = $gateways[ $gateway_id ];
|
||
|
||
// Check if gateway has process_payment method
|
||
if ( ! method_exists( $gateway, 'process_payment' ) ) {
|
||
error_log( '[WooNooW] Gateway does not have process_payment method: ' . $gateway_id );
|
||
return new \WP_Error( 'no_process_method', sprintf( __( 'Gateway does not support payment processing: %s', 'woonoow' ), $gateway_id ) );
|
||
}
|
||
|
||
try {
|
||
// Mark as admin-created for gateways that need to know
|
||
$order->update_meta_data( '_woonoow_admin_created', true );
|
||
$order->save();
|
||
|
||
// Set flag for gateways to detect admin context
|
||
add_filter( 'woonoow/is_admin_order', '__return_true' );
|
||
|
||
error_log( '[WooNooW] Processing payment for order #' . $order->get_id() . ' with gateway: ' . $gateway_id );
|
||
|
||
// Call gateway's process_payment method
|
||
$result = $gateway->process_payment( $order->get_id() );
|
||
|
||
// Clean up filter
|
||
remove_filter( 'woonoow/is_admin_order', '__return_true' );
|
||
|
||
// Store result metadata
|
||
if ( is_array( $result ) ) {
|
||
if ( isset( $result['redirect'] ) ) {
|
||
$order->update_meta_data( '_woonoow_payment_redirect', $result['redirect'] );
|
||
}
|
||
|
||
if ( isset( $result['result'] ) && $result['result'] === 'success' ) {
|
||
$order->add_order_note( __( 'Payment gateway processing completed via WooNooW', 'woonoow' ) );
|
||
error_log( '[WooNooW] Payment processing succeeded for order #' . $order->get_id() );
|
||
} elseif ( isset( $result['result'] ) && $result['result'] === 'failure' ) {
|
||
$message = isset( $result['message'] ) ? $result['message'] : __( 'Payment processing failed', 'woonoow' );
|
||
$order->add_order_note( sprintf( __( 'Payment gateway error: %s', 'woonoow' ), $message ) );
|
||
error_log( '[WooNooW] Payment processing failed for order #' . $order->get_id() . ': ' . $message );
|
||
}
|
||
|
||
$order->save();
|
||
}
|
||
|
||
return $result;
|
||
|
||
} catch ( \Throwable $e ) {
|
||
error_log( '[WooNooW] Payment processing exception for order #' . $order->get_id() . ': ' . $e->getMessage() );
|
||
$order->add_order_note( sprintf( __( 'Payment gateway exception: %s', 'woonoow' ), $e->getMessage() ) );
|
||
$order->save();
|
||
|
||
return new \WP_Error( 'gateway_exception', $e->getMessage() );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get payment gateway metadata for display
|
||
* Extracts common payment metadata like VA number, QR code, expiry, etc.
|
||
*
|
||
* @param \WC_Order $order Order object
|
||
* @return array Payment metadata
|
||
*/
|
||
private static function get_payment_metadata( $order ): array {
|
||
$meta = [];
|
||
|
||
// Common metadata keys used by Indonesian payment gateways
|
||
$meta_keys = apply_filters( 'woonoow/payment_meta_keys', [
|
||
// Tripay
|
||
'_tripay_payment_pay_code' => __( 'Payment Code', 'woonoow' ),
|
||
'_tripay_payment_reference' => __( 'Reference', 'woonoow' ),
|
||
'_tripay_payment_expired_time' => __( 'Expires At', 'woonoow' ),
|
||
'_tripay_payment_amount' => __( 'Amount', 'woonoow' ),
|
||
'_tripay_payment_type' => __( 'Payment Type', 'woonoow' ),
|
||
|
||
// Duitku
|
||
'_duitku_reference' => __( 'Reference', 'woonoow' ),
|
||
'_duitku_va_number' => __( 'VA Number', 'woonoow' ),
|
||
'_duitku_payment_url' => __( 'Payment URL', 'woonoow' ),
|
||
|
||
// Xendit
|
||
'_xendit_invoice_id' => __( 'Invoice ID', 'woonoow' ),
|
||
'_xendit_invoice_url' => __( 'Invoice URL', 'woonoow' ),
|
||
'_xendit_expiry_date' => __( 'Expires At', 'woonoow' ),
|
||
|
||
// WooNooW
|
||
'_woonoow_payment_redirect' => __( 'Payment URL', 'woonoow' ),
|
||
'_woonoow_admin_created' => __( 'Created via Admin', 'woonoow' ),
|
||
], $order );
|
||
|
||
foreach ( $meta_keys as $key => $label ) {
|
||
$value = $order->get_meta( $key );
|
||
if ( ! empty( $value ) ) {
|
||
// Format timestamps
|
||
if ( strpos( $key, 'expired_time' ) !== false || strpos( $key, 'expiry' ) !== false ) {
|
||
if ( is_numeric( $value ) ) {
|
||
$value = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $value );
|
||
}
|
||
}
|
||
|
||
// Format amounts (currency)
|
||
if ( strpos( $key, 'amount' ) !== false && is_numeric( $value ) ) {
|
||
$value = wc_price( $value, [ 'currency' => $order->get_currency() ] );
|
||
}
|
||
|
||
// Format booleans
|
||
if ( is_bool( $value ) ) {
|
||
$value = $value ? __( 'Yes', 'woonoow' ) : __( 'No', 'woonoow' );
|
||
}
|
||
|
||
$meta[] = [
|
||
'key' => $key,
|
||
'label' => $label,
|
||
'value' => $value,
|
||
];
|
||
}
|
||
}
|
||
|
||
return $meta;
|
||
}
|
||
|
||
/**
|
||
* POST /woonoow/v1/orders/{id}/retry-payment
|
||
* Retry payment gateway processing for an order
|
||
*/
|
||
public static function retry_payment( WP_REST_Request $req ): WP_REST_Response {
|
||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||
return new WP_REST_Response( [ 'error' => 'forbidden' ], 403 );
|
||
}
|
||
|
||
$id = absint( $req['id'] );
|
||
if ( ! $id ) {
|
||
return new WP_REST_Response( [ 'error' => 'invalid_id' ], 400 );
|
||
}
|
||
|
||
$order = wc_get_order( $id );
|
||
if ( ! $order ) {
|
||
return new WP_REST_Response( [ 'error' => 'not_found' ], 404 );
|
||
}
|
||
|
||
$payment_method = $order->get_payment_method();
|
||
if ( empty( $payment_method ) ) {
|
||
return new WP_REST_Response( [
|
||
'error' => 'no_payment_method',
|
||
'message' => __( 'Order has no payment method', 'woonoow' )
|
||
], 400 );
|
||
}
|
||
|
||
// Only allow retry for pending/on-hold orders
|
||
$status = $order->get_status();
|
||
if ( ! in_array( $status, [ 'pending', 'on-hold', 'failed' ] ) ) {
|
||
return new WP_REST_Response( [
|
||
'error' => 'invalid_status',
|
||
'message' => sprintf(
|
||
__( 'Cannot retry payment for order with status: %s', 'woonoow' ),
|
||
$status
|
||
)
|
||
], 400 );
|
||
}
|
||
|
||
// Add order note
|
||
$order->add_order_note( __( 'Payment retry requested via WooNooW Admin', 'woonoow' ) );
|
||
|
||
// Block WooCommerce analytics tracking during ALL save operations (prevents 30s delay)
|
||
// This needs to wrap the entire operation because process_payment_gateway() also calls save()
|
||
add_filter('pre_http_request', function($preempt, $args, $url) {
|
||
if (strpos($url, 'pixel.wp.com') !== false || strpos($url, 'stats.wp.com') !== false) {
|
||
return new \WP_Error('http_request_blocked', 'WooCommerce analytics blocked');
|
||
}
|
||
return $preempt;
|
||
}, PHP_INT_MAX, 3);
|
||
|
||
$order->save();
|
||
|
||
// Trigger payment processing and capture result (this also calls save() internally)
|
||
$result = self::process_payment_gateway( $order, $payment_method );
|
||
|
||
// Remove filter after ALL operations complete
|
||
remove_all_filters('pre_http_request');
|
||
|
||
// Check if payment processing failed
|
||
if ( is_wp_error( $result ) ) {
|
||
return new WP_REST_Response( [
|
||
'error' => 'payment_failed',
|
||
'message' => $result->get_error_message()
|
||
], 400 );
|
||
}
|
||
|
||
return new WP_REST_Response( [
|
||
'success' => true,
|
||
'message' => __( 'Payment processing retried', 'woonoow' )
|
||
], 200 );
|
||
}
|
||
|
||
/**
|
||
* POST /woonoow/v1/coupons/validate
|
||
* Validate a coupon code and calculate discount
|
||
*/
|
||
public static function validate_coupon( WP_REST_Request $req ): WP_REST_Response {
|
||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||
return new WP_REST_Response( [ 'error' => 'forbidden' ], 403 );
|
||
}
|
||
|
||
$code = sanitize_text_field( $req['code'] );
|
||
if ( empty( $code ) ) {
|
||
return new WP_REST_Response( [
|
||
'valid' => false,
|
||
'error' => __( 'Coupon code is required', 'woonoow' )
|
||
], 400 );
|
||
}
|
||
|
||
// Get coupon
|
||
$coupon = new \WC_Coupon( $code );
|
||
|
||
// Check if coupon exists
|
||
if ( ! $coupon->get_id() ) {
|
||
return new WP_REST_Response( [
|
||
'valid' => false,
|
||
'error' => __( 'Coupon does not exist', 'woonoow' )
|
||
], 200 );
|
||
}
|
||
|
||
// Check if coupon is valid (not expired, usage limits, etc.)
|
||
$validation = $coupon->is_valid();
|
||
if ( is_wp_error( $validation ) ) {
|
||
return new WP_REST_Response( [
|
||
'valid' => false,
|
||
'error' => $validation->get_error_message()
|
||
], 200 );
|
||
}
|
||
|
||
// Get coupon details
|
||
$discount_type = $coupon->get_discount_type();
|
||
$amount = $coupon->get_amount();
|
||
$subtotal = floatval( $req['subtotal'] ?? 0 );
|
||
|
||
// Calculate discount amount
|
||
$discount_amount = 0;
|
||
if ( $discount_type === 'percent' ) {
|
||
$discount_amount = ( $subtotal * $amount ) / 100;
|
||
} elseif ( $discount_type === 'fixed_cart' ) {
|
||
$discount_amount = min( $amount, $subtotal );
|
||
} elseif ( $discount_type === 'fixed_product' ) {
|
||
// For product-level discounts, we'd need item details
|
||
$discount_amount = $amount;
|
||
}
|
||
|
||
// Format response
|
||
return new WP_REST_Response( [
|
||
'valid' => true,
|
||
'code' => $coupon->get_code(),
|
||
'discount_type' => $discount_type,
|
||
'amount' => $amount,
|
||
'discount_amount' => $discount_amount,
|
||
'description' => $coupon->get_description(),
|
||
'individual_use' => $coupon->get_individual_use(),
|
||
'free_shipping' => $coupon->get_free_shipping(),
|
||
'minimum_amount' => $coupon->get_minimum_amount(),
|
||
'maximum_amount' => $coupon->get_maximum_amount(),
|
||
'usage_count' => $coupon->get_usage_count(),
|
||
'usage_limit' => $coupon->get_usage_limit(),
|
||
'expiry_date' => $coupon->get_date_expires() ? $coupon->get_date_expires()->date( 'Y-m-d' ) : null,
|
||
], 200 );
|
||
}
|
||
} |