$s->get('store_country', ''), 'state' => $s->get('store_state', ''), 'city' => $s->get('store_city', ''), 'postcode' => $s->get('store_postcode', ''), ]; } if ($basis === 'shipping' && !empty($shippingAddress) && !empty($shippingAddress['country'])) { return $shippingAddress; } return $billingAddress ?: []; } public static function calculate($items, $address, $taxSettings = null) { if (!TaxModule::isTaxEnabled()) { return null; } $country = Arr::get($address, 'country', ''); $state = Arr::get($address, 'state', ''); $city = Arr::get($address, 'city', ''); $postCode = Arr::get($address, 'postcode', ''); if (empty($country)) { return null; } // Build line-item array in the shape TaxCalculator expects: // key fields: post_id, object_id, subtotal, discount_total, shipping_charge, quantity. // Fee lines (is_fee) mirror Cart::buildFeeCartItem(): post_id/object_id 0, // quantity 1 — TaxCalculator applies the first product's rates to them. $lineItems = []; foreach ($items as $item) { $isFee = !empty($item['is_fee']); $subtotal = (int) Arr::get($item, 'subtotal', 0); $discountTotal = (int) Arr::get($item, 'discount_total', 0); $qty = (int) Arr::get($item, 'qty', Arr::get($item, 'quantity', 1)); $lineItems[] = [ // Order-item id, when the caller has one. Custom lines all share // post_id/object_id 0:0, so the id is the only per-line identity // the patch step can match on. 'id' => $isFee ? 0 : (int) Arr::get($item, 'id', 0), 'post_id' => $isFee ? 0 : (int) Arr::get($item, 'post_id', Arr::get($item, 'product_id', 0)), 'object_id' => $isFee ? 0 : (int) Arr::get($item, 'object_id', Arr::get($item, 'variation_id', 0)), 'subtotal' => $subtotal, 'discount_total' => $discountTotal, 'shipping_charge' => (int) Arr::get($item, 'shipping_charge', 0), 'quantity' => $isFee ? 1 : $qty, 'is_fee' => $isFee, 'title' => (string) Arr::get($item, 'title', ''), 'other_info' => Arr::get($item, 'other_info', []), ]; } if (empty($lineItems)) { return null; } try { if ($taxSettings === null) { $taxSettings = (new TaxModule())->getSettings(); } $taxCalculator = new TaxCalculator($lineItems, [ 'inclusive' => false, 'country' => $country, 'state' => $state, 'city' => $city, 'postcode' => $postCode, 'tax_rounding' => Arr::get($taxSettings, 'tax_rounding', 'item'), ]); $taxedLines = $taxCalculator->getTaxedLines(); // Split fee lines out of the taxed lines — same convention as // TaxModule::calculateCartTax(). Fee lines never enter line_items // (no post_id to patch); their tax is reported via fee_tax / // fee_tax_lines. Note: getTotalTax() and getTaxLinesByRates() // already include the fee portion; getExclusiveTaxTotal() excludes // fee lines — identical to the aggregates checkout stores. $productLines = []; $feeTax = 0; $feeTaxLines = []; foreach ($taxedLines as $taxedLine) { if (!empty($taxedLine['is_fee'])) { $feeLineTax = (int) Arr::get($taxedLine, 'tax_amount', 0); $feeTax += $feeLineTax; $feeTaxLines[] = [ 'label' => Arr::get($taxedLine, 'title', ''), 'tax_amount' => $feeLineTax, 'inclusive' => (bool) Arr::get($taxedLine, 'line_meta.tax_config.inclusive', false), ]; } else { $productLines[] = $taxedLine; } } $lineItemsResult = array_values(array_map(function ($lineItem) { return [ 'id' => (int) Arr::get($lineItem, 'id', 0), 'post_id' => (int) Arr::get($lineItem, 'post_id', 0), 'object_id' => (int) Arr::get($lineItem, 'object_id', 0), 'tax_amount' => (int) Arr::get($lineItem, 'tax_amount', 0), 'line_meta' => Arr::get($lineItem, 'line_meta', []), 'recurring_tax' => (int) Arr::get($lineItem, 'other_info.recurring_tax', 0), 'signup_fee_tax' => (int) Arr::get($lineItem, 'other_info.signup_fee_tax', 0), 'signup_fee_tax_config' => Arr::get($lineItem, 'signup_fee_tax_config', []), ]; }, $productLines)); return [ 'tax_total' => $taxCalculator->getTotalTax(), 'exclusive_tax_total' => $taxCalculator->getExclusiveTaxTotal(), 'shipping_tax' => $taxCalculator->getShippingTax(), 'shipping_tax_lines' => $taxCalculator->getShippingTaxByRates(), 'fee_tax' => $feeTax, 'fee_tax_lines' => $feeTaxLines, 'tax_lines' => $taxCalculator->getTaxLinesByRates(), 'tax_behavior' => $taxCalculator->getTaxBehaviorValue(), 'store_tax_behavior' => $taxCalculator->getStoreTaxBehaviorValue(), 'tax_country' => $taxCalculator->getTaxCountry(), 'line_items' => $lineItemsResult, ]; } catch (\Exception $e) { // Log but never block order creation fluent_cart_warning_log( 'AdminOrderTaxService: calculation failed', get_class($e) . ': ' . wp_strip_all_tags($e->getMessage()), ['module_name' => 'tax', 'log_type' => 'api'] ); return null; } } }