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();
?>
shouldRateStrikethrough($isReversed, isset($rate['inclusive']) ? (bool)$rate['inclusive'] : $taxData['is_inclusive'], $rcMode) ? ' is-reversed' : ''; ?>
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();
?>
shouldRateStrikethrough($isReversed, $rate['inclusive'], $rcMode) ? ' is-reversed' : ''; ?>
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';
}
}