feat(orders): Add WooCommerce-native calculation endpoints
## Problem:
1. No shipping service options (UPS Ground, UPS Express, etc.)
2. Tax not calculated (11% PPN not showing)
3. Manual cost calculation instead of using WooCommerce core
## Root Cause:
Current implementation manually sets shipping costs from static config:
```php
$shipping_cost = $method->get_option( 'cost', 0 );
$ship_item->set_total( $shipping_cost );
```
This doesn't work for:
- Live rate methods (UPS, FedEx) - need dynamic calculation
- Tax calculation - WooCommerce needs proper context
- Service-level rates (UPS Ground vs Express)
## Solution: Use WooCommerce Native Calculation
### New Endpoints:
1. **POST /woonoow/v1/shipping/calculate**
- Calculates real-time shipping rates
- Uses WooCommerce cart + customer address
- Returns all available methods with costs
- Supports live rate plugins (UPS, FedEx)
- Returns service-level options
2. **POST /woonoow/v1/orders/preview**
- Previews order totals before creation
- Calculates: subtotal, shipping, tax, discounts, total
- Uses WooCommerce cart engine
- Respects tax settings and rates
- Applies coupons correctly
### How It Works:
```php
// Temporarily use WooCommerce cart
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product_id, $qty );
WC()->customer->set_shipping_address( $address );
WC()->cart->calculate_shipping();
WC()->cart->calculate_totals();
// Get calculated rates
$packages = WC()->shipping()->get_packages();
foreach ( $packages as $package ) {
$rates = $package['rates']; // UPS Ground, UPS Express, etc.
}
// Get totals with tax
$total = WC()->cart->get_total();
$tax = WC()->cart->get_total_tax();
```
### Benefits:
- ✅ Live shipping rates work
- ✅ Service-level options appear
- ✅ Tax calculated correctly
- ✅ Coupons applied properly
- ✅ Uses WooCommerce core logic
- ✅ No reinventing the wheel
### Next Steps (Frontend):
1. Call `/shipping/calculate` when address changes
2. Show service options in dropdown
3. Call `/orders/preview` to show totals with tax
4. Update UI to display tax breakdown
This commit is contained in:
@@ -97,6 +97,31 @@ class OrdersController {
|
||||
'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',
|
||||
@@ -1197,6 +1222,176 @@ class OrdersController {
|
||||
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 {
|
||||
// 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 ) ) {
|
||||
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'] ?? '' );
|
||||
WC()->customer->set_shipping_address( $shipping['address_1'] ?? '' );
|
||||
WC()->customer->set_shipping_address_2( $shipping['address_2'] ?? '' );
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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 = [];
|
||||
|
||||
Reference in New Issue
Block a user