add_action('rest_api_init', function () { register_rest_route('wc/store/v1', '/get-cart', [ 'methods' => 'GET', 'callback' => 'get_cart_data_with_metadata', 'permission_callback' => '__return_true', ]); }); function _safe_get($src, $key, $default = '') { if (is_array($src) && array_key_exists($key, $src)) return $src[$key]; if (is_object($src) && isset($src->$key)) return $src->$key; return $default; } /** * Normalize any mixed array/object structure to associative array */ function _to_array($value) { return json_decode(json_encode($value), true); } function get_cart_data_with_metadata(WP_REST_Request $request) { try { // -------- Cart token -------- $cart_token = $request->get_header('cart-token') ?: $request->get_param('cart-token'); // -------- Shipping address (request headers/params fallback to store base) -------- $base_loc = wc_get_base_location(); $shipping_address = [ 'country' => strtoupper(sanitize_text_field($request->get_param('shipping_country') ?: $request->get_header('shipping-country') ?: ($base_loc['country'] ?? ''))), 'state' => strtoupper(sanitize_text_field($request->get_param('shipping_state') ?: $request->get_header('shipping-state') ?: ($base_loc['state'] ?? ''))), 'postcode' => sanitize_text_field($request->get_param('shipping_postcode') ?: $request->get_header('shipping-postcode') ?: ''), 'city' => sanitize_text_field($request->get_param('shipping_city') ?: $request->get_header('shipping-city') ?: ''), 'address_1' => sanitize_text_field($request->get_param('shipping_address_1') ?: $request->get_header('shipping-address-1') ?: ''), 'address_2' => sanitize_text_field($request->get_param('shipping_address_2') ?: $request->get_header('shipping-address-2') ?: ''), ]; $requested_rate_id = sanitize_text_field($request->get_param('shipping_rate_id') ?: $request->get_header('shipping-rate-id')); // -------- Load current cart from Store API internally -------- $store_request = new WP_REST_Request('GET', '/wc/store/v1/cart'); if ($cart_token) { $store_request->set_header('Cart-Token', sanitize_text_field($cart_token)); } $response = rest_do_request($store_request); if (is_wp_error($response)) { return rest_respond(false, 'Failed to load cart from Store API.', $response->get_error_message()); } // Normalize response data to associative arrays $server = rest_get_server(); $data_raw = $server->response_to_data($response, false); $data = _to_array($data_raw); if (empty($data['items']) || !is_array($data['items'])) { return new WP_REST_Response(['success' => false, 'message' => 'Cart is empty.'], 200); } // -------- Discount calculation (existing logic) -------- $rules = get_option('yaydp_product_pricing_rules', []); $discount_results = []; $total_discount = 0.0; foreach ($rules as $rule) { if (empty($rule['is_enabled'])) continue; $rule_discount = calculate_rule_discount($rule, $data['items']); if (!empty($rule_discount['applicable'])) { $discount_results[] = $rule_discount; $total_discount += (float) $rule_discount['discount_amount']; } } // Apply discounts (keeps items as arrays, augment them) $cart_items_with_discounts = apply_discounts_to_items($data['items'], $discount_results); // Inject per-item tax fields if present from Store API items foreach ($cart_items_with_discounts as &$item) { // item may contain 'line_total_tax' or totals array -> normalize $item_tax = 0.0; if (isset($item['line_total_tax'])) { $item_tax = (float) $item['line_total_tax']; } elseif (isset($item['totals']) && is_array($item['totals']) && isset($item['totals']['line_total_tax'])) { $item_tax = (float) $item['totals']['line_total_tax']; } elseif (isset($item['taxes']) && is_array($item['taxes'])) { // fallback: sum taxes array $item_tax = array_sum(array_map('floatval', $item['taxes'])); } $item['tax_total'] = round($item_tax, 2); // ensure numeric fields $qty = (int) ($item['quantity'] ?? 1); $orig_price = (float) ($item['prices']['regular_price'] ?? ($item['prices']['price'] ?? 0)); $line_total = round($orig_price * $qty, 2); $item['line_total'] = $line_total; $item['total_with_tax'] = round($line_total + $item['tax_total'], 2); } unset($item); // break reference // -------- Subtotals -------- $cart_subtotal = 0.0; foreach ($cart_items_with_discounts as $it) { $cart_subtotal += (float) ($it['line_total'] ?? 0); } $discount_total = min((float)$total_discount, (float)$cart_subtotal); $subtotal_after_discount = max(0.0, $cart_subtotal - $discount_total); // --- Build shipping_rates formatted data --- $shipping_rates_output = []; if (WC()->shipping()) { $shipping = WC()->shipping; $shipping->load_shipping_methods(); // Build a single package $package = [ 'contents' => [], 'contents_cost' => 0, 'destination' => [ 'address_1' => $shipping_address['address_1'] ?? '', 'address_2' => $shipping_address['address_2'] ?? '', 'city' => $shipping_address['city'] ?? '', 'state' => $shipping_address['state'] ?? '', 'postcode' => $shipping_address['postcode'] ?? '', 'country' => $shipping_address['country'] ?? 'GB', ], ]; // Add cart items to the shipping package foreach ($cart_items_with_discounts as $key => $it) { $product = wc_get_product((int)($it['product_id'] ?? 0)); if ($product && is_a($product, 'WC_Product')) { $qty = (int)($it['quantity'] ?? 1); $line_total = (float)$it['line_total'] ?? 0; $package['contents'][$key] = [ 'data' => $product, 'quantity' => $qty, 'line_total' => $line_total, 'line_subtotal' => $line_total, ]; $package['contents_cost'] += $line_total; } } $packages = [$package]; $shipping->calculate_shipping($packages); $packages_with_rates = $shipping->get_packages(); if (!empty($packages_with_rates)) { foreach ($packages_with_rates as $pkg_index => $pkg) { $destination = $pkg['destination'] ?? []; $items_list = []; $items_label = []; foreach ($pkg['contents'] as $key => $item) { $product_name = $item['data']->get_name(); $qty = $item['quantity']; $items_list[] = [ 'key' => $key, 'name' => $product_name, 'quantity' => $qty, ]; $items_label[] = sprintf('%s × %d', $product_name, $qty); } $rates_list = []; $rates = $pkg['rates'] ?? []; foreach ($rates as $rate_id => $rate_obj) { if (!is_a($rate_obj, 'WC_Shipping_Rate')) continue; $taxes = (array)$rate_obj->get_taxes(); $tax_total = array_sum($taxes); $rates_list[] = [ 'rate_id' => $rate_id, 'name' => $rate_obj->get_label(), 'description' => '', 'delivery_time' => '', 'price' => (string)round($rate_obj->get_cost(), 2), 'taxes' => (string)round($tax_total, 2), 'instance_id' => (int)$rate_obj->get_instance_id(), 'method_id' => $rate_obj->get_method_id(), 'meta_data' => [ [ 'key' => 'Items', 'value' => implode(', ', $items_label), ] ], 'selected' => true, // choose default for now 'currency_code' => get_woocommerce_currency(), 'currency_symbol' => get_woocommerce_currency_symbol(), 'currency_minor_unit' => wc_get_price_decimals(), 'currency_decimal_separator' => wc_get_price_decimal_separator(), 'currency_thousand_separator' => wc_get_price_thousand_separator(), 'currency_prefix' => get_woocommerce_currency_symbol(), 'currency_suffix' => '', ]; } $shipping_rates_output[] = [ 'package_id' => $pkg_index, 'name' => 'Shipment ' . ($pkg_index + 1), 'destination' => $destination, 'items' => $items_list, 'shipping_rates' => $rates_list, ]; } } } // --- Calculate tax lines --- $tax_lines = []; $tax_total = 0; $total_tax = 0.0; if (WC()->cart && !WC()->cart->is_empty()) { // Ensure taxes are recalculated WC()->cart->calculate_totals(); // Get tax totals from WooCommerce $tax_totals = WC()->cart->get_tax_totals(); // array of WC_Tax_Rate objects foreach ($tax_totals as $rate_code => $tax_obj) { $rate_label = $tax_obj->label; $rate_percent = $tax_obj->formatted_rate; // e.g. "20%" $tax_amount = wc_format_decimal($tax_obj->amount, wc_get_price_decimals()); $tax_total += floatval($tax_amount); $tax_lines[] = [ 'name' => $rate_label ?: $rate_percent, 'price' => (string)$tax_amount, 'rate' => $rate_percent ?: $rate_label, ]; } // If no tax lines were found, still include one with 0 if (empty($tax_lines)) { $tax_lines[] = [ 'name' => '0%', 'price' => '0', 'rate' => '0%', ]; } } // Fallback: sum item line_total_tax values if ($total_tax <= 0) { foreach ($data['items'] as $it) { $it_arr = _to_array($it); if (isset($it_arr['line_total_tax'])) { $total_tax += (float)$it_arr['line_total_tax']; } elseif (isset($it_arr['totals']['line_total_tax'])) { $total_tax += (float)$it_arr['totals']['line_total_tax']; } elseif (!empty($it_arr['taxes']) && is_array($it_arr['taxes'])) { $total_tax += array_sum(array_map('floatval', $it_arr['taxes'])); } } } // Add shipping tax (calculated from chosen rate) $total_tax += $shipping_tax_total; // If no breakdown_by_rate was assembled, provide a generic single-entry bucket if (empty($tax_breakdown_by_rate)) { $tax_breakdown_by_rate = ['1' => round($total_tax, 2)]; } else { // Round each foreach ($tax_breakdown_by_rate as $k => $v) { $tax_breakdown_by_rate[$k] = round((float)$v, 2); } } $tax_info = [ 'prices_include_tax' => (bool) $prices_include_tax, 'breakdown_by_rate' => $tax_breakdown_by_rate, 'total_tax' => round($total_tax, 2), ]; // -------- Totals -------- $shipping_total = $shipping_cost_ex_tax + $shipping_tax_total; $grand_total = round($subtotal_after_discount, 2) + round($shipping_total, 2) + round($total_tax, 2); // -------- RESPONSE -------- return new WP_REST_Response([ 'success' => true, 'cart_items' => $cart_items_with_discounts, 'discounts' => [ 'total_discount' => round($discount_total, 2), 'applied_rules' => $discount_results, ], 'shipping_rates' => $shipping_rates_output, 'tax_lines' => $tax_lines, 'totals' => [ 'subtotal' => round($cart_subtotal, 2), 'subtotal_after_discount' => round($subtotal_after_discount, 2), 'shipping_total' => $shipping_total, 'tax_total' => round($total_tax, 2), 'grand_total' => round($grand_total, 2), ], ], 200); } catch (Throwable $e) { return new WP_REST_Response([ 'success' => false, 'message' => 'Calculation failed.', 'error' => $e->getMessage(), 'trace' => WP_DEBUG ? $e->getTraceAsString() : null, ], 200); } } /* ------------------------- Your existing discount helpers (kept mostly unchanged) ------------------------- */ function calculate_rule_discount($rule, $cart_items) { $rule_type = $rule['type'] ?? ''; switch ($rule_type) { case 'buy_x_get_y': return calculate_buy_x_get_y_discount($rule, $cart_items); case 'bulk_pricing': return function_exists('calculate_bulk_pricing_discount') ? calculate_bulk_pricing_discount($rule, $cart_items) : array('applicable' => false, 'discount_amount' => 0); case 'simple_adjustment': return function_exists('calculate_simple_adjustment_discount') ? calculate_simple_adjustment_discount($rule, $cart_items) : array('applicable' => false, 'discount_amount' => 0); default: return array('applicable' => false, 'discount_amount' => 0); } } function calculate_buy_x_get_y_discount($rule, $cart_items) { $buy_products = $rule['buy_products']['filters'][0]['value'] ?? array(); $get_products = $rule['get_products']['filters'][0]['value'] ?? array(); $pricing = $rule['pricing'] ?? array(); $required_quantity = $rule['buy_products']['filters'][0]['quantity'] ?? 1; $get_quantity = $rule['get_products']['filters'][0]['quantity'] ?? 1; $discount_value = $pricing['value'] ?? 100; $discount_type = $pricing['type'] ?? 'percentage_discount'; $repeat = $pricing['repeat'] ?? 1; $effect_type = $pricing['affected_items']['effect_type'] ?? 'lowest_price'; $affected_items_type = $pricing['affected_items']['type'] ?? 'whole_bundle'; $buy_product_ids = array_map(function($item) { return (int)(is_array($item) && isset($item['value']) ? $item['value'] : $item); }, $buy_products); $qualifying_quantity = 0; foreach ($cart_items as $item) { if (in_array($item['product_id'], $buy_product_ids)) { $qualifying_quantity += $item['quantity']; } } if ($qualifying_quantity < $required_quantity) { return array('applicable' => false, 'discount_amount' => 0); } $free_sets = floor($qualifying_quantity / $required_quantity); $total_free_items = $free_sets * $get_quantity; $discount_amount = 0; $discountable_items = []; foreach ($cart_items as $item) { $product = wc_get_product($item['product_id']); if ($product) { $discountable_items[] = [ 'product_id' => $item['product_id'], 'price' => (float)$product->get_price(), 'quantity' => (int)$item['quantity'] ]; } } usort($discountable_items, function($a, $b) { return $a['price'] <=> $b['price']; }); $items_to_discount = min($total_free_items, array_sum(array_column($discountable_items, 'quantity'))); foreach ($discountable_items as $item) { if ($items_to_discount <= 0) break; $discount_qty = min($items_to_discount, $item['quantity']); $discount_amount += $item['price'] * $discount_qty * ($discount_value / 100); $items_to_discount -= $discount_qty; } return [ 'applicable' => true, 'rule_id' => $rule['rule_id'] ?? null, 'rule_name' => $rule['name'] ?? null, 'discount_amount' => $discount_amount, 'free_items' => $total_free_items, 'qualifying_quantity' => $qualifying_quantity, 'pricing_details' => [ 'buy_quantity' => $required_quantity, 'get_quantity' => $get_quantity, 'discount_type' => $discount_type, 'discount_value' => $discount_value, 'effect_type' => $effect_type, 'repeat' => $repeat, ], ]; } function apply_discounts_to_items($cart_items, $discount_results) { foreach ($cart_items as &$item) { $product = function_exists('wc_get_product') ? wc_get_product($item['product_id']) : null; $item['original_price'] = $product ? (float) $product->get_price() : (float) ($item['price'] ?? 0); $item['discounted_price'] = $item['original_price']; $item['discount_amount'] = 0; $item['applied_rules'] = []; foreach ($discount_results as $rule_result) { if (!empty($rule_result['applicable'])) { $item['applied_rules'][] = $rule_result['rule_name'] ?? ''; } } } unset($item); return $cart_items; } /* ------- small helper - respond uniform error format ------- */ function rest_respond($success, $message = '', $error = null) { $payload = ['success' => $success, 'message' => $message]; if ($error) $payload['error'] = $error; return new WP_REST_Response($payload, 200); }