module = $module; } public function renderTaxRow($cart, $atts = ''): void { if ($this->getCheckoutTaxBreakdownDisplayMode() === 'simplified') { return; } $taxAmount = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); $reversedTaxAmount = (int) Arr::get($cart->checkout_data, 'tax_data.reverse_charge_tax_total', 0); $isReverseCharge = $this->module->isReverseChargeCheckout($cart->checkout_data); $taxLines = Arr::get($cart->checkout_data, 'tax_data.tax_lines', []); $taxCountry = Arr::get($cart->checkout_data, 'tax_data.tax_country', ''); $taxLabel = Arr::get($taxLines, '0.label', ''); if (!$taxLabel) { $taxLabel = $taxCountry ? TaxModule::getCountryTaxTitle($taxCountry) : __('Tax', 'fluent-cart'); } ?>
  • >
  • getCheckoutTaxBreakdownDisplayMode() === 'simplified') { return ''; } $shippingTax = (int) Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0); $isReverseCharge = $this->module->isReverseChargeCheckout($cart->checkout_data); $rcShippingTax = (int) Arr::get($cart->checkout_data, 'tax_data.reverse_charge_shipping_tax', 0); if ($shippingTax <= 0 && (!$isReverseCharge || $rcShippingTax <= 0)) { return ''; } $displayAmount = $isReverseCharge ? $rcShippingTax : $shippingTax; $storeBehavior = (int) Arr::get($cart->checkout_data, 'tax_data.store_tax_behavior', Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2)); $isInclusive = $storeBehavior === 2; ?>
  • > >
  • checkout_data, 'tax_data', []); $taxTotal = (int) Arr::get($taxData, 'tax_total', 0); $exclusiveTaxTotal = (int) Arr::get($taxData, 'exclusive_tax_total', $taxTotal); $feeTaxLines = (array) Arr::get($taxData, 'fee_tax_lines', []); $shippingTax = (int) Arr::get($taxData, 'shipping_tax', 0); $isReverseCharge = $this->module->isReverseChargeCheckout($cart->checkout_data); $reversedTaxTotalDisplay = (int) Arr::get($taxData, 'reverse_charge_tax_total', 0); $taxRateLines = (array) Arr::get($taxData, 'tax_lines', []); $shippingTaxLines = (array) Arr::get($taxData, 'shipping_tax_lines', []); $inclusiveFeeTax = 0; $exclusiveFeeTax = 0; foreach ($feeTaxLines as $feeTaxLine) { if (!empty($feeTaxLine['inclusive'])) { $inclusiveFeeTax += (int) Arr::get($feeTaxLine, 'tax_amount', 0); } else { $exclusiveFeeTax += (int) Arr::get($feeTaxLine, 'tax_amount', 0); } } // Fallback: if no fee_tax_lines (old cart data), use aggregate fee_tax as exclusive if (empty($feeTaxLines)) { $exclusiveFeeTax = (int) Arr::get($taxData, 'fee_tax', 0); } $inclusiveTax = max(0, $taxTotal - $exclusiveTaxTotal - $exclusiveFeeTax - $inclusiveFeeTax); $productExclusiveTax = max(0, $exclusiveTaxTotal); $isShippingInclusive = TaxSummaryHelper::isShippingTaxInclusiveFromTaxData( is_array($taxData) ? $taxData : [] ); $payableTax = $productExclusiveTax + $exclusiveFeeTax + ($isShippingInclusive ? 0 : $shippingTax); $totalOrderTax = $inclusiveTax + $inclusiveFeeTax + ($isShippingInclusive ? $shippingTax : 0) + $payableTax; $feeRows = TaxSummaryHelper::buildFeeTaxLineRows($feeTaxLines); $feeCount = count($feeRows); if (empty($feeTaxLines) && (int) Arr::get($taxData, 'fee_tax', 0) > 0) { $feeCount = 1; } $productTaxRowCount = !empty($taxRateLines) ? count($taxRateLines) : (int) ($inclusiveTax > 0) + (int) ($productExclusiveTax > 0); $rowCount = $productTaxRowCount + $feeCount + (int) ($shippingTax > 0); $shouldShowBreakdown = !empty($taxRateLines) || !empty($shippingTaxLines) || $rowCount >= 2 || ($rowCount === 1 && !($payableTax > 0 || $inclusiveTax > 0 || $inclusiveFeeTax > 0)); // Build folded per-rate rows via shared helper. Cart tax_lines carry only `label` // (no percent), so we pre-augment each line with `rate_label` = "Label (X%)" so the // helper picks it up and produces the same display as the old inline block. $foldedSource = []; foreach ($taxRateLines as $rateKey => $rateLine) { $ratePercent = (float) Arr::get($rateLine, 'rate_percent', 0); $rateLine['rate_label'] = (string) Arr::get($rateLine, 'label', ''); if ($ratePercent > 0) { $rateLine['rate_label'] .= ' (' . Helper::formatTaxRatePercent($ratePercent) . '%)'; } $foldedSource[$rateKey] = $rateLine; } // Fixed-mode reverse charge leaves tax-inclusive prices (and their embedded VAT) // untouched — only dynamic mode reverses the inclusive portion. Exclude inclusive // lines from the map in fixed mode so the rate rows sum to the reversed total. $rcNonDynamic = $isReverseCharge && Arr::get($taxData, 'reverse_charge_price_mode', 'fixed') !== 'dynamic'; $rateBaseMap = TaxSummaryHelper::computeRateBaseMap((array) ($cart->cart_data ?: []), $rcNonDynamic); if ($isReverseCharge) { // Under reverse charge the stored tax_lines amounts are zeroed at calc time, // but the original per-rate amounts survive in item line_meta. Restore them // (and the pre-zeroing shipping lines) so the box renders the same // breakdown-by-rate table as a normal order. foreach ($foldedSource as $rateKey => &$foldedLine) { $rid = (int) Arr::get($foldedLine, 'rate_id', $rateKey); if (isset($rateBaseMap[$rid])) { $foldedLine['tax_amount'] = (int) round($rateBaseMap[$rid]['tax']); } } unset($foldedLine); if ($rcNonDynamic) { // Rates present only on inclusive lines have nothing reversible in // fixed mode — drop them instead of rendering a zero row. foreach (array_keys($foldedSource) as $rateKey) { $rid = (int) Arr::get($foldedSource[$rateKey], 'rate_id', $rateKey); if (!isset($rateBaseMap[$rid])) { unset($foldedSource[$rateKey]); } } } $shippingTaxLines = (array) Arr::get($taxData, 'reverse_charge_shipping_tax_lines', []); } $checkoutRateRows = TaxSummaryHelper::buildFoldedRateRows( $foldedSource, $shippingTaxLines, 'tax_amount', $isShippingInclusive, $rateBaseMap ); // Inclusive shipping tax follows the store global tax mode — when shipping is // priced inclusive its tax is baked into the shipping price, so it counts as // "of which included in prices". Keeps includedInPrices + payableTax === totalOrderTax. $includedInPrices = $inclusiveTax + $inclusiveFeeTax + ($isShippingInclusive ? $shippingTax : 0); $displayMode = $this->getCheckoutTaxBreakdownDisplayMode(); $isSimplified = $displayMode === 'simplified'; $simpleLabel = $this->getTaxDisplayLabel(); if ($isReverseCharge) { $simpleValue = __('Reverse charge', 'fluent-cart'); } elseif ($payableTax === 0 && $totalOrderTax > 0) { $suffix = (string) Arr::get($this->module->getSettings(), 'price_suffix_included', ''); if ($suffix === '') { $suffix = __('(incl.)', 'fluent-cart'); } /* translators: %1$s: formatted tax amount, %2$s: inclusive suffix */ $simpleValue = sprintf(__('%1$s %2$s', 'fluent-cart'), html_entity_decode(Helper::toDecimal($totalOrderTax), ENT_QUOTES, 'UTF-8'), $suffix); } else { $simpleValue = html_entity_decode(Helper::toDecimal($payableTax), ENT_QUOTES, 'UTF-8'); } $tooltipId = 'fct-tax-summary-tooltip-' . Helper::getUidSerial(); ?>
  • ▾
    0) + (int) ($rcExclusiveNonShip > 0) + (int) ($rcShippingDisplay > 0); ?> = 2) : ?> 0) : ?>
    0) : ?>
    0) : ?>
    0) : ?>
    0 && $shouldShowBreakdown) : ?>
    0 && $shouldShowBreakdown) : ?>
    0 && $shouldShowBreakdown) : ?>
    0 && $shouldShowBreakdown) : ?>
    0) : ?>
    0 && $includedInPrices > 0) : ?>
    0) : ?>
    0 || $inclusiveFeeTax > 0) : ?>
  • getCheckoutTaxBreakdownDisplayMode(); if ($mode === 'simplified') { return; } $item = Arr::get($data, 'item', []); $rates = $this->normalizeCheckoutLineTaxRates($item); if (empty($rates)) { return; } $cart = CartHelper::getCart(); $isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; $rcMode = $this->module->getEffectiveRcMode(); $this->renderTaxBadges($rates, $isReversed, $rcMode); } public function renderCheckoutSetupFeeTaxLabel($data): void { $mode = $this->getCheckoutTaxBreakdownDisplayMode(); if ($mode === 'simplified') { return; } $item = Arr::get($data, 'item', []); $signupFeeTaxConfig = Arr::get($item, 'signup_fee_tax_config', []); $rates = Arr::get($signupFeeTaxConfig, 'rates', []); if (empty($rates) || !is_array($rates)) { return; } $isInclusive = (bool) Arr::get($signupFeeTaxConfig, 'inclusive', false); $normalizedRates = []; foreach ($rates as $rate) { $taxAmount = (int) Arr::get($rate, 'tax_amount', 0); $ratePercent = (float) Arr::get($rate, 'rate_percent', 0); if ($taxAmount <= 0 && $ratePercent <= 0) { continue; } $normalizedRates[] = [ 'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')), 'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent), 'inclusive' => $isInclusive, 'tax_amount' => $taxAmount, ]; } if (empty($normalizedRates)) { return; } $cart = CartHelper::getCart(); $isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; $rcMode = $this->module->getEffectiveRcMode(); $this->renderTaxBadges($normalizedRates, $isReversed, $rcMode); } private function renderTaxBadges(array $normalizedRates, $isReversed = false, $rcMode = 'fixed'): void { // Fixed-mode reverse charge: inclusive-priced lines keep their gross price and // no VAT is charged, so an "incl. X" badge would be misleading — hide those rates. if ($isReversed && $rcMode !== 'dynamic') { $normalizedRates = array_values(array_filter($normalizedRates, function ($rate) { return empty($rate['inclusive']); })); } if (empty($normalizedRates)) { return; } ?>
    0) : ?> shouldRateStrikethrough($isReversed, $rate['inclusive'], $rcMode) ? ' is-reversed' : ''; ?>
    $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')), 'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent), 'tax_amount' => $taxAmount, 'display_base' => $isInclusive ? max(0, $taxableAmount - $taxAmount) : $taxableAmount, ]; } if (empty($rates)) { return null; } $setupFee = (int) Arr::get($item, 'other_info.signup_fee', 0); $totalTax = array_sum(array_map(function ($rate) { return (int) $rate['tax_amount']; }, $rates)); return [ 'rates' => $rates, 'is_inclusive' => $isInclusive, 'total_tax' => $totalTax, 'display_total' => $isInclusive ? $setupFee : $setupFee + $totalTax, 'primary_label' => Arr::get($rates, '0.short_label', __('Tax', 'fluent-cart')), ]; } public function renderCheckoutSetupFeeTaxTooltip($data): void { $mode = $this->getCheckoutTaxBreakdownDisplayMode(); if ($mode === 'simplified') { return; } $item = Arr::get($data, 'item', []); $taxData = $this->getSetupFeeTaxData($item); if (!$taxData) { return; } $cart = CartHelper::getCart(); $isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; $rcMode = $this->module->getEffectiveRcMode(); $rates = $taxData['rates']; $isInclusive = $taxData['is_inclusive']; $displayTotal = $taxData['display_total']; if ($isReversed) { if ($taxData['is_inclusive'] && $rcMode === 'dynamic') { $displayTotal = $taxData['display_total'] - $taxData['total_tax']; } else { $displayTotal = (int) Arr::get($data, 'item.other_info.signup_fee', 0); } } $tooltipId = 'fct-item-tax-tooltip-' . Helper::getUidSerial(); ?>
    getCheckoutTaxBreakdownDisplayMode(); if ($mode === 'simplified') { return; } $item = Arr::get($data, 'item', []); $taxData = $this->getSetupFeeTaxData($item); if (!$taxData) { return; } $isInclusive = $taxData['is_inclusive']; $totalTax = $taxData['total_tax']; $primaryLabel = $taxData['primary_label']; $priceNote = $isInclusive ? sprintf( /* translators: %1$s: tax label */ __('%1$s incl.', 'fluent-cart'), $primaryLabel ) : sprintf( /* translators: %1$s: tax amount, %2$s: tax label */ __('+ %1$s %2$s', 'fluent-cart'), Helper::toDecimal($totalTax), strtolower($primaryLabel) ); ?> getCheckoutTaxBreakdownDisplayMode(); if ($mode === 'simplified') { return; } $item = Arr::get($data, 'item', []); $rates = $this->normalizeCheckoutLineTaxRates($item); if (empty($rates)) { return; } $cart = CartHelper::getCart(); $isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; $rcMode = $this->module->getEffectiveRcMode(); $isInclusive = (bool) Arr::get($rates, '0.inclusive', false); $itemSubtotal = (int) Arr::get($item, 'line_total', Arr::get($item, 'subtotal', 0)); $itemTaxAmount = array_sum(array_map(function ($rate) { return (int) Arr::get($rate, 'tax_amount', 0); }, $rates)); if ($isReversed) { if ($isInclusive && $rcMode === 'dynamic') { $displayTotal = $itemSubtotal - $itemTaxAmount; } else { $displayTotal = $itemSubtotal; } } else { $displayTotal = $isInclusive ? $itemSubtotal : $itemSubtotal + $itemTaxAmount; } $tooltipId = 'fct-item-tax-tooltip-' . Helper::getUidSerial(); ?>
    getCheckoutTaxBreakdownDisplayMode(); if ($mode === 'simplified') { return; } $item = Arr::get($data, 'item', []); $rates = $this->normalizeCheckoutLineTaxRates($item); if (empty($rates)) { return; } $isInclusive = (bool) Arr::get($rates, '0.inclusive', false); $itemTaxAmount = array_sum(array_map(function ($rate) { return (int) Arr::get($rate, 'tax_amount', 0); }, $rates)); $primaryLabel = Arr::get($rates, '0.short_label', __('Tax', 'fluent-cart')); $priceNote = $isInclusive ? sprintf( /* translators: %1$s: tax label */ __('%1$s incl.', 'fluent-cart'), $primaryLabel ) : sprintf( /* translators: %1$s: tax amount, %2$s: tax label */ __('+ %1$s %2$s', 'fluent-cart'), Helper::toDecimal($itemTaxAmount), strtolower($primaryLabel) ); ?>
    0) { $rates = (array) Arr::get($item, 'line_meta.tax_config.rates', []); $actualTaxTotal = (int) array_sum(array_map('intval', array_column($rates, 'tax_amount'))); $diff = abs($rcAdjustment - $actualTaxTotal); if ($diff > 0 && $diff <= $quantity) { $shouldShow = true; } } // Case 2 — generic: unit_price * qty already doesn't match subtotal // (future dynamic pricing or any other module that stores a rounded unit_price) if (!$shouldShow && $subtotal > 0) { $diff = abs(($unitPrice * $quantity) - $subtotal); if ($diff > 0 && $diff <= $quantity) { $shouldShow = true; } } if (!$shouldShow) { return; } $tooltipId = 'fct-unit-price-rounding-' . Helper::getUidSerial(); ?>
    module->getSettings(), 'checkout_tax_breakdown_display', 'itemized'); return $mode === 'simplified' ? 'simplified' : 'itemized'; } protected function getTaxDisplayLabel(): string { $label = trim((string) Arr::get($this->module->getSettings(), 'tax_display_label', '')); return $label !== '' ? $label : __('Tax', 'fluent-cart'); } protected function normalizeCheckoutLineTaxRates($item): array { $rates = Arr::get($item, 'line_meta.tax_config.rates', []); if (empty($rates) || !is_array($rates)) { return []; } $isInclusive = (bool) Arr::get($item, 'line_meta.tax_config.inclusive', false); $normalizedRates = []; foreach ($rates as $rate) { $taxAmount = (int) Arr::get($rate, 'tax_amount', 0); $ratePercent = (float) Arr::get($rate, 'rate_percent', 0); $taxableAmount = (int) Arr::get($rate, 'taxable_amount', 0); if ($taxAmount <= 0 && $ratePercent <= 0) { continue; } $normalizedRates[] = [ 'label' => (string) Arr::get($rate, 'label', __('Tax', 'fluent-cart')), 'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')), 'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent), 'tax_amount' => $taxAmount, 'display_base' => $isInclusive ? max(0, $taxableAmount - $taxAmount) : $taxableAmount, 'inclusive' => $isInclusive, ]; } return $normalizedRates; } protected function getCheckoutLineTaxShortLabel($label): string { $label = trim((string) $label); if (!$label) { return __('Tax', 'fluent-cart'); } return $label; } private function shouldRateStrikethrough($isReversed, $rateIsInclusive, $rcMode): bool { if (!$isReversed) { return false; } return !$rateIsInclusive || $rcMode === 'dynamic'; } }