Files
WooNooW/includes/Api/CheckoutController.php
Dwindi Ramadhana 274c3d35e1 fix(checkout): fix disabled country/state and add public countries API
Issues fixed:
1. Country field was disabled when API failed (length 0)
   - Changed: disabled={countries.length <= 1} → disabled={countries.length === 1}
   - Only disables in single-country mode now

2. State field was disabled when no preloaded states
   - Changed: Falls back to text input instead of disabled SearchableSelect
   - Allows manual state entry for countries without state list

3. /countries API required admin permission
   - Added public /countries endpoint to CheckoutController
   - Uses permission_callback __return_true for customer checkout access
   - Returns countries, states, and default_country
2026-01-08 14:02:13 +07:00

776 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace WooNooW\Api;
use WP_Error;
use WP_REST_Request;
use WC_Order;
use WC_Product;
use WC_Shipping_Zones;
use WC_Shipping_Rate;
if (!defined('ABSPATH')) { exit; }
class CheckoutController {
/**
* Register REST routes for checkout quote & submit
*/
public static function register() {
$namespace = 'woonoow/v1';
register_rest_route($namespace, '/checkout/quote', [
'methods' => 'POST',
'callback' => [ new self(), 'quote' ],
'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ], // consider nonce check later
]);
register_rest_route($namespace, '/checkout/submit', [
'methods' => 'POST',
'callback' => [ new self(), 'submit' ],
'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ], // consider capability/nonce
]);
register_rest_route($namespace, '/checkout/fields', [
'methods' => 'POST',
'callback' => [ new self(), 'get_fields' ],
'permission_callback' => [ \WooNooW\Api\Permissions::class, 'anon_or_wp_nonce' ],
]);
// Public countries endpoint for customer checkout form
register_rest_route($namespace, '/countries', [
'methods' => 'GET',
'callback' => [ new self(), 'get_countries' ],
'permission_callback' => '__return_true', // Public - needed for checkout
]);
// Public order view endpoint for thank you page
register_rest_route($namespace, '/checkout/order/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [ new self(), 'get_order' ],
'permission_callback' => '__return_true', // Public, validated via order_key
'args' => [
'key' => [
'type' => 'string',
'required' => false,
],
],
]);
}
/**
* Build a quote for the given payload:
* {
* items: [{ product_id, variation_id?, qty, meta?[] }],
* billing: {...},
* shipping: {..., ship_to_different?},
* coupons: ["CODE"],
* shipping_method: "flat_rate:1" | "free_shipping:3" | ...
* }
*/
public function quote(WP_REST_Request $r): array {
$__t0 = microtime(true);
$payload = $this->sanitize_payload($r);
// Allow a fully accurate quote using Woo's Cart mechanics (slower, but parity)
$useAccurate = (bool) apply_filters('woonoow/quote/accurate', false);
if ($useAccurate) {
$resp = $this->accurate_quote_via_wc_cart($payload);
if (!headers_sent()) {
header('Server-Timing: app;dur=' . round((microtime(true) - $__t0) * 1000, 1));
}
return $resp;
}
// Build a transient cart-like calculation using Woo helpers.
// We will sum line totals and attempt shipping/tax estimates.
$lines = [];
$subtotal = 0.0;
$discount = 0.0; // coupons not applied to calculations in this v0 (placeholder)
$shipping_total = 0.0;
$tax_total = 0.0;
foreach ($payload['items'] as $line) {
$product = $this->load_product($line);
if ($product instanceof WP_Error) {
return ['error' => $product->get_error_message()];
}
$qty = max(1, (int)($line['qty'] ?? 1));
$price = (float) wc_get_price_to_display($product);
$line_total = $price * $qty;
$lines[] = [
'product_id' => $product->get_id(),
'name' => $product->get_name(),
'qty' => $qty,
'price' => $price,
'line_total' => $line_total,
];
$subtotal += $line_total;
}
// --- simple coupon handling (fast path) ---
if (!empty($payload['coupons'])) {
foreach ($payload['coupons'] as $code) {
$coupon = new \WC_Coupon($code);
if ($coupon->get_id()) {
$type = $coupon->get_discount_type(); // 'percent', 'fixed_cart', etc.
$amount = (float) $coupon->get_amount();
if ($type === 'percent') {
$discount += ($amount / 100.0) * $subtotal;
} elseif ($type === 'fixed_cart') {
$discount += min($amount, $subtotal);
}
// NOTE: fixed_product & restrictions are ignored in FastQuote
}
}
}
$discount = min($discount, $subtotal);
// Simple shipping estimate using zones (optional, best-effort).
if (!empty($payload['shipping']['postcode']) && !empty($payload['shipping']['country'])) {
$shipping_total = $this->estimate_shipping($payload['shipping'], $payload['shipping_method'] ?? null);
}
$grand = max(0, $subtotal - $discount + $shipping_total + $tax_total);
if (!headers_sent()) {
header('Server-Timing: app;dur=' . round((microtime(true) - $__t0) * 1000, 1));
}
return [
'ok' => true,
'items' => $lines,
'totals' => [
'subtotal' => wc_format_decimal($subtotal, wc_get_price_decimals()),
'discount_total' => wc_format_decimal($discount, wc_get_price_decimals()),
'shipping_total' => wc_format_decimal($shipping_total, wc_get_price_decimals()),
'tax_total' => wc_format_decimal($tax_total, wc_get_price_decimals()),
'grand_total' => wc_format_decimal($grand, wc_get_price_decimals()),
'currency' => get_woocommerce_currency(),
'currency_symbol' => get_woocommerce_currency_symbol(),
'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(),
],
];
}
/**
* Public order view endpoint for thank you page
* Validates access via order_key (for guests) or logged-in customer ID
* GET /checkout/order/{id}?key=wc_order_xxx
*/
public function get_order(WP_REST_Request $r): array {
$order_id = absint($r['id']);
$order_key = sanitize_text_field($r->get_param('key') ?? '');
if (!$order_id) {
return ['error' => __('Invalid order ID', 'woonoow')];
}
$order = wc_get_order($order_id);
if (!$order) {
return ['error' => __('Order not found', 'woonoow')];
}
// Validate access: order_key must match OR user must be logged in and own the order
$valid_key = $order_key && hash_equals($order->get_order_key(), $order_key);
$valid_owner = is_user_logged_in() && get_current_user_id() === $order->get_customer_id();
if (!$valid_key && !$valid_owner) {
return ['error' => __('Unauthorized access to order', 'woonoow')];
}
// Build order items
$items = [];
foreach ($order->get_items() as $item) {
$product = $item->get_product();
$items[] = [
'id' => $item->get_id(),
'product_id' => $product ? $product->get_id() : 0,
'name' => $item->get_name(),
'qty' => (int) $item->get_quantity(),
'price' => (float) $item->get_total() / max(1, $item->get_quantity()),
'total' => (float) $item->get_total(),
'image' => $product ? wp_get_attachment_image_url($product->get_image_id(), 'thumbnail') : null,
];
}
return [
'ok' => true,
'id' => $order->get_id(),
'number' => $order->get_order_number(),
'status' => $order->get_status(),
'subtotal' => (float) $order->get_subtotal(),
'shipping_total' => (float) $order->get_shipping_total(),
'tax_total' => (float) $order->get_total_tax(),
'total' => (float) $order->get_total(),
'currency' => $order->get_currency(),
'currency_symbol' => get_woocommerce_currency_symbol($order->get_currency()),
'payment_method' => $order->get_payment_method_title(),
'billing' => [
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
],
'items' => $items,
];
}
/**
* Submit an order:
* {
* items: [{ product_id, variation_id?, qty, meta?[] }],
* billing: {...},
* shipping: {..., ship_to_different?},
* coupons: ["CODE"],
* shipping_method: "flat_rate:1",
* payment_method: "cod" | "bacs" | ...
* }
*/
public function submit(WP_REST_Request $r): array {
$__t0 = microtime(true);
$payload = $this->sanitize_payload($r);
if (empty($payload['items'])) {
return ['error' => __('No items provided', 'woonoow')];
}
// Create order
$order = wc_create_order();
if (is_wp_error($order)) {
return ['error' => $order->get_error_message()];
}
// Set customer ID if user is logged in
if (is_user_logged_in()) {
$user_id = get_current_user_id();
$order->set_customer_id($user_id);
// Update user's billing information from checkout data
if (!empty($payload['billing'])) {
$billing = $payload['billing'];
// Update first name and last name
if (!empty($billing['first_name'])) {
update_user_meta($user_id, 'first_name', sanitize_text_field($billing['first_name']));
update_user_meta($user_id, 'billing_first_name', sanitize_text_field($billing['first_name']));
}
if (!empty($billing['last_name'])) {
update_user_meta($user_id, 'last_name', sanitize_text_field($billing['last_name']));
update_user_meta($user_id, 'billing_last_name', sanitize_text_field($billing['last_name']));
}
// Update billing phone
if (!empty($billing['phone'])) {
update_user_meta($user_id, 'billing_phone', sanitize_text_field($billing['phone']));
}
// Update billing email
if (!empty($billing['email'])) {
update_user_meta($user_id, 'billing_email', sanitize_email($billing['email']));
}
}
} else {
// Guest checkout - check if auto-register is enabled
$customer_settings = \WooNooW\Compat\CustomerSettingsProvider::get_settings();
$auto_register = $customer_settings['auto_register_members'] ?? false;
if ($auto_register && !empty($payload['billing']['email'])) {
$email = sanitize_email($payload['billing']['email']);
// Check if user already exists
$existing_user = get_user_by('email', $email);
if ($existing_user) {
// User exists - link order to them
$order->set_customer_id($existing_user->ID);
} else {
// Create new user account
$password = wp_generate_password(12, true, true);
$userdata = [
'user_login' => $email,
'user_email' => $email,
'user_pass' => $password,
'first_name' => sanitize_text_field($payload['billing']['first_name'] ?? ''),
'last_name' => sanitize_text_field($payload['billing']['last_name'] ?? ''),
'display_name' => trim((sanitize_text_field($payload['billing']['first_name'] ?? '') . ' ' . sanitize_text_field($payload['billing']['last_name'] ?? ''))) ?: $email,
'role' => 'customer', // WooCommerce customer role
];
$new_user_id = wp_insert_user($userdata);
if (!is_wp_error($new_user_id)) {
// Link order to new user
$order->set_customer_id($new_user_id);
// Store temp password in user meta for email template
// The real password is already set via wp_insert_user
update_user_meta($new_user_id, '_woonoow_temp_password', $password);
// AUTO-LOGIN: Set authentication cookie so user is logged in after page reload
wp_set_auth_cookie($new_user_id, true);
wp_set_current_user($new_user_id);
// Set WooCommerce customer billing data
$customer = new \WC_Customer($new_user_id);
if (!empty($payload['billing']['first_name'])) $customer->set_billing_first_name(sanitize_text_field($payload['billing']['first_name']));
if (!empty($payload['billing']['last_name'])) $customer->set_billing_last_name(sanitize_text_field($payload['billing']['last_name']));
if (!empty($payload['billing']['email'])) $customer->set_billing_email(sanitize_email($payload['billing']['email']));
if (!empty($payload['billing']['phone'])) $customer->set_billing_phone(sanitize_text_field($payload['billing']['phone']));
if (!empty($payload['billing']['address_1'])) $customer->set_billing_address_1(sanitize_text_field($payload['billing']['address_1']));
if (!empty($payload['billing']['city'])) $customer->set_billing_city(sanitize_text_field($payload['billing']['city']));
if (!empty($payload['billing']['state'])) $customer->set_billing_state(sanitize_text_field($payload['billing']['state']));
if (!empty($payload['billing']['postcode'])) $customer->set_billing_postcode(sanitize_text_field($payload['billing']['postcode']));
if (!empty($payload['billing']['country'])) $customer->set_billing_country(sanitize_text_field($payload['billing']['country']));
$customer->save();
// Send new account email (WooCommerce will handle this automatically via hook)
do_action('woocommerce_created_customer', $new_user_id, $userdata, $password);
}
}
}
}
// Add items
foreach ($payload['items'] as $line) {
$product = $this->load_product($line);
if (is_wp_error($product)) {
return ['error' => $product->get_error_message()];
}
$qty = max(1, (int)($line['qty'] ?? 1));
$args = [];
if (!empty($line['meta']) && is_array($line['meta'])) {
$args['item_meta_array'] = $line['meta'];
}
$order->add_product($product, $qty, $args);
}
// Addresses
if (!empty($payload['billing'])) {
$order->set_address($this->only_address_fields($payload['billing']), 'billing');
}
if (!empty($payload['shipping'])) {
$ship = $payload['shipping'];
// If ship_to_different is false, copy billing
if (empty($ship['ship_to_different'])) {
$ship = $this->only_address_fields($payload['billing'] ?: []);
}
$order->set_address($this->only_address_fields($ship), 'shipping');
}
// Coupons (besteffort using Woo Coupon objects)
if (!empty($payload['coupons']) && is_array($payload['coupons'])) {
foreach ($payload['coupons'] as $code) {
try {
$coupon = new \WC_Coupon(wc_clean(wp_unslash($code)));
if ($coupon->get_id()) {
$order->apply_coupon($coupon);
}
} catch (\Throwable $e) {
// ignore invalid in v0
}
}
}
// Shipping (besteffort estimate)
if (!empty($payload['shipping_method'])) {
$rate = $this->find_shipping_rate_for_order($order, $payload['shipping_method']);
if ($rate instanceof WC_Shipping_Rate) {
$item = new \WC_Order_Item_Shipping();
$item->set_props([
'method_title' => $rate->get_label(),
'method_id' => $rate->get_method_id(),
'instance_id' => $rate->get_instance_id(),
'total' => $rate->get_cost(),
'taxes' => $rate->get_taxes(),
]);
$order->add_item($item);
}
}
// Payment method
if (!empty($payload['payment_method'])) {
$order->set_payment_method($payload['payment_method']);
}
// Totals
$order->calculate_totals();
// Mirror Woo hooks so extensions still work (but not thankyou which outputs HTML)
do_action('woocommerce_checkout_create_order', $order->get_id(), $order);
$order->save();
if (!headers_sent()) {
header('Server-Timing: app;dur=' . round((microtime(true) - $__t0) * 1000, 1));
}
// Clear WooCommerce cart after successful order placement
// This ensures the cart page won't re-populate from server session
if (function_exists('WC') && WC()->cart) {
WC()->cart->empty_cart();
}
return [
'ok' => true,
'order_id' => $order->get_id(),
'order_key' => $order->get_order_key(),
'status' => $order->get_status(),
'pay_url' => $order->get_checkout_payment_url(),
'thankyou_url' => $order->get_checkout_order_received_url(),
];
}
/**
* Get checkout fields with all filters applied
* Accepts: { items: [...], is_digital_only?: bool }
* Returns fields with required, hidden, etc. based on addons + cart context
*/
public function get_fields(WP_REST_Request $r): array {
$json = $r->get_json_params();
$items = isset($json['items']) && is_array($json['items']) ? $json['items'] : [];
$is_digital_only = isset($json['is_digital_only']) ? (bool) $json['is_digital_only'] : false;
// Initialize WooCommerce checkout if not already
if (!WC()->checkout()) {
WC()->initialize_session();
WC()->initialize_cart();
}
// Get checkout fields with all filters applied
$fields = WC()->checkout()->get_checkout_fields();
$formatted = [];
foreach ($fields as $fieldset_key => $fieldset) {
foreach ($fieldset as $key => $field) {
// Check if field should be hidden
$hidden = false;
// Hide shipping fields if digital only (your existing logic)
if ($is_digital_only && $fieldset_key === 'shipping') {
$hidden = true;
}
// Check if addon/filter explicitly hides this field
if (isset($field['class']) && is_array($field['class'])) {
if (in_array('hidden', $field['class']) || in_array('hide', $field['class'])) {
$hidden = true;
}
}
// Respect 'enabled' flag if set by addons
if (isset($field['enabled']) && !$field['enabled']) {
$hidden = true;
}
$formatted[] = [
'key' => $key,
'fieldset' => $fieldset_key, // billing, shipping, account, order
'type' => $field['type'] ?? 'text',
'label' => $field['label'] ?? '',
'placeholder' => $field['placeholder'] ?? '',
'required' => $field['required'] ?? false,
'hidden' => $hidden,
'class' => $field['class'] ?? [],
'priority' => $field['priority'] ?? 10,
'options' => $field['options'] ?? null, // For select fields
'custom' => !in_array($key, $this->get_standard_field_keys()), // Flag custom fields
'autocomplete'=> $field['autocomplete'] ?? '',
'validate' => $field['validate'] ?? [],
// New fields for dynamic rendering
'input_class' => $field['input_class'] ?? [],
'custom_attributes' => $field['custom_attributes'] ?? [],
'default' => $field['default'] ?? '',
// For searchable_select type
'search_endpoint' => $field['search_endpoint'] ?? null,
'search_param' => $field['search_param'] ?? 'search',
'min_chars' => $field['min_chars'] ?? 2,
];
}
}
// Sort by priority
usort($formatted, function($a, $b) {
return $a['priority'] <=> $b['priority'];
});
return [
'ok' => true,
'fields' => $formatted,
'is_digital_only' => $is_digital_only,
];
}
/**
* Get list of standard WooCommerce field keys
*/
private function get_standard_field_keys(): array {
return [
'billing_first_name',
'billing_last_name',
'billing_company',
'billing_country',
'billing_address_1',
'billing_address_2',
'billing_city',
'billing_state',
'billing_postcode',
'billing_phone',
'billing_email',
'shipping_first_name',
'shipping_last_name',
'shipping_company',
'shipping_country',
'shipping_address_1',
'shipping_address_2',
'shipping_city',
'shipping_state',
'shipping_postcode',
'order_comments',
];
}
/** ----------------- Helpers ----------------- **/
private function accurate_quote_via_wc_cart(array $payload): array {
if (!WC()->customer) { WC()->customer = new \WC_Customer(get_current_user_id(), true); }
if (!WC()->cart) { WC()->cart = new \WC_Cart(); }
// Address context for taxes/shipping rules - set temporarily without saving to user profile
$ship = !empty($payload['shipping']) ? $payload['shipping'] : $payload['billing'];
if (!empty($payload['billing'])) {
foreach (['country','state','postcode','city','address_1','address_2'] as $k) {
$setter = 'set_billing_' . $k;
if (method_exists(WC()->customer, $setter) && isset($payload['billing'][$k])) {
WC()->customer->{$setter}(wc_clean($payload['billing'][$k]));
}
}
}
if (!empty($ship)) {
foreach (['country','state','postcode','city','address_1','address_2'] as $k) {
$setter = 'set_shipping_' . $k;
if (method_exists(WC()->customer, $setter) && isset($ship[$k])) {
WC()->customer->{$setter}(wc_clean($ship[$k]));
}
}
}
// DO NOT save customer data - only use for quote calculation
// WC()->customer->save(); // REMOVED - should not update user profile during checkout
WC()->cart->empty_cart(true);
foreach ($payload['items'] as $line) {
$product = $this->load_product($line);
if (is_wp_error($product)) {
return ['error' => $product->get_error_message()];
}
WC()->cart->add_to_cart($product->get_id(), max(1, (int) ($line['qty'] ?? 1)));
}
if (!empty($payload['coupons'])) {
foreach ($payload['coupons'] as $code) {
try {
WC()->cart->apply_coupon($code);
} catch (\Throwable $e) {
// ignore invalid in v0
}
}
}
WC()->cart->calculate_totals();
$subtotal = (float) WC()->cart->get_subtotal();
$discount = (float) WC()->cart->get_discount_total();
$shipping_total = (float) WC()->cart->get_shipping_total();
$tax_total = (float) WC()->cart->get_total_tax();
$grand = (float) WC()->cart->get_total('edit');
$lines = [];
foreach (WC()->cart->get_cart() as $ci) {
$p = $ci['data'];
$qty = (int) $ci['quantity'];
$price = (float) wc_get_price_to_display($p);
$lines[] = [
'product_id' => $p->get_id(),
'name' => $p->get_name(),
'qty' => $qty,
'price' => $price,
'line_total' => $price * $qty,
];
}
return [
'ok' => true,
'items' => $lines,
'totals' => [
'subtotal' => wc_format_decimal($subtotal, wc_get_price_decimals()),
'discount_total' => wc_format_decimal($discount, wc_get_price_decimals()),
'shipping_total' => wc_format_decimal($shipping_total, wc_get_price_decimals()),
'tax_total' => wc_format_decimal($tax_total, wc_get_price_decimals()),
'grand_total' => wc_format_decimal($grand, wc_get_price_decimals()),
'currency' => get_woocommerce_currency(),
'currency_symbol' => get_woocommerce_currency_symbol(),
'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(),
],
];
}
private function sanitize_payload(WP_REST_Request $r): array {
$json = $r->get_json_params();
$items = isset($json['items']) && is_array($json['items']) ? $json['items'] : [];
$billing = isset($json['billing']) && is_array($json['billing']) ? $json['billing'] : [];
$shipping = isset($json['shipping']) && is_array($json['shipping']) ? $json['shipping'] : [];
$coupons = isset($json['coupons']) && is_array($json['coupons']) ? array_map('wc_clean', $json['coupons']) : [];
return [
'items' => array_map(function ($i) {
return [
'product_id' => isset($i['product_id']) ? (int) $i['product_id'] : 0,
'variation_id' => isset($i['variation_id']) ? (int) $i['variation_id'] : 0,
'qty' => isset($i['qty']) ? (int) $i['qty'] : 1,
'meta' => isset($i['meta']) && is_array($i['meta']) ? $i['meta'] : [],
];
}, $items),
'billing' => $billing,
'shipping'=> $shipping,
'coupons' => $coupons,
'shipping_method' => isset($json['shipping_method']) ? wc_clean($json['shipping_method']) : null,
'payment_method' => isset($json['payment_method']) ? wc_clean($json['payment_method']) : null,
];
}
private function load_product(array $line) {
$pid = (int)($line['variation_id'] ?? 0) ?: (int)($line['product_id'] ?? 0);
if (!$pid) {
return new WP_Error('bad_item', __('Invalid product id', 'woonoow'));
}
$product = wc_get_product($pid);
if (!$product || !$product->is_purchasable()) {
return new WP_Error('bad_item', __('Product not purchasable', 'woonoow'));
}
return $product;
}
private function only_address_fields(array $src): array {
$keys = ['first_name','last_name','company','address_1','address_2','city','state','postcode','country','email','phone'];
$out = [];
foreach ($keys as $k) {
if (isset($src[$k])) $out[$k] = wc_clean(wp_unslash($src[$k]));
}
return $out;
}
private function estimate_shipping(array $address, ?string $chosen_method): float {
$country = wc_clean($address['country'] ?? '');
$postcode = wc_clean($address['postcode'] ?? '');
$state = wc_clean($address['state'] ?? '');
$city = wc_clean($address['city'] ?? '');
$cache_key = 'wnw_ship_' . md5(json_encode([$country, $state, $postcode, $city, (string) $chosen_method]));
$cached = wp_cache_get($cache_key, 'woonoow');
if ($cached !== false) { return (float) $cached; }
if (!$country) return 0.0;
$packages = [[
'destination' => compact('country','state','postcode','city'),
'contents_cost' => 0, // not exact in v0
'contents' => [],
'applied_coupons' => [],
'user' => ['ID' => get_current_user_id()],
]];
$zone = \WC_Shipping_Zones::get_zone_matching_package($packages[0]);
$methods = $zone ? $zone->get_shipping_methods(true) : [];
$cost = 0.0;
foreach ($methods as $method) {
if (!empty($chosen_method)) {
$id = $method->id . ':' . $method->get_instance_id();
if ($id === $chosen_method && method_exists($method, 'get_rates_for_package')) {
$rates = $method->get_rates_for_package($packages[0]);
if (!empty($rates)) {
$rate = reset($rates);
$cost = (float) $rate->get_cost();
break;
}
}
}
}
wp_cache_set($cache_key, $cost, 'woonoow', 60); // cache for 60 seconds
return $cost;
}
private function find_shipping_rate_for_order(WC_Order $order, string $chosen) {
$shipping = $order->get_address('shipping');
$packages = [[
'destination' => [
'country' => $shipping['country'] ?? '',
'state' => $shipping['state'] ?? '',
'postcode' => $shipping['postcode'] ?? '',
'city' => $shipping['city'] ?? '',
],
'contents' => [],
'applied_coupons' => [],
'user' => ['ID' => $order->get_user_id()],
]];
$zone = \WC_Shipping_Zones::get_zone_matching_package($packages[0]);
if (!$zone) return null;
foreach ($zone->get_shipping_methods(true) as $method) {
$id = $method->id . ':' . $method->get_instance_id();
if ($id === $chosen && method_exists($method, 'get_rates_for_package')) {
$rates = $method->get_rates_for_package($packages[0]);
if (!empty($rates)) {
return reset($rates);
}
}
}
return null;
}
/**
* Get countries and states for checkout form
* Public endpoint - no authentication required
*/
public function get_countries(): array {
$wc_countries = WC()->countries;
// Get allowed selling countries
$allowed = $wc_countries->get_allowed_countries();
// Format for frontend
$countries = [];
foreach ($allowed as $code => $name) {
$countries[] = [
'code' => $code,
'name' => $name,
];
}
// Get states for all allowed countries
$states = [];
foreach (array_keys($allowed) as $country_code) {
$country_states = $wc_countries->get_states($country_code);
if (!empty($country_states) && is_array($country_states)) {
$states[$country_code] = $country_states;
}
}
// Get default country
$default_country = $wc_countries->get_base_country();
return [
'countries' => $countries,
'states' => $states,
'default_country' => $default_country,
];
}
}