| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Services\Renderer; |
| 4 |
|
| 5 |
use FluentCart\App\Helpers\Helper; |
| 6 |
use FluentCart\Framework\Support\Arr; |
| 7 |
use FluentCart\App\Helpers\CartHelper; |
| 8 |
use FluentCart\App\Modules\Tax\TaxModule; |
| 9 |
use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper; |
| 10 |
|
| 11 |
class TaxRenderer |
| 12 |
{ |
| 13 |
protected $module; |
| 14 |
|
| 15 |
public function __construct(TaxModule $module) |
| 16 |
{ |
| 17 |
$this->module = $module; |
| 18 |
} |
| 19 |
|
| 20 |
public function renderTaxRow($cart, $atts = ''): void |
| 21 |
{ |
| 22 |
if ($this->getCheckoutTaxBreakdownDisplayMode() === 'simplified') { |
| 23 |
return; |
| 24 |
} |
| 25 |
|
| 26 |
$taxAmount = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0); |
| 27 |
$reversedTaxAmount = (int) Arr::get($cart->checkout_data, 'tax_data.reverse_charge_tax_total', 0); |
| 28 |
$isReverseCharge = $this->module->isReverseChargeCheckout($cart->checkout_data); |
| 29 |
$taxLines = Arr::get($cart->checkout_data, 'tax_data.tax_lines', []); |
| 30 |
$taxCountry = Arr::get($cart->checkout_data, 'tax_data.tax_country', ''); |
| 31 |
$taxLabel = Arr::get($taxLines, '0.label', ''); |
| 32 |
|
| 33 |
if (!$taxLabel) { |
| 34 |
$taxLabel = $taxCountry ? TaxModule::getCountryTaxTitle($taxCountry) : __('Tax', 'fluent-cart'); |
| 35 |
} |
| 36 |
?> |
| 37 |
<li <?php echo $atts; ?>> |
| 38 |
<span class="fct_summary_label"> |
| 39 |
<?php echo esc_html($taxLabel); ?> |
| 40 |
</span> |
| 41 |
<span class="fct_summary_value"> |
| 42 |
<?php if ($isReverseCharge && $taxAmount === 0) : ?> |
| 43 |
<?php |
| 44 |
/* translators: %1$s: formatted reversed tax amount */ |
| 45 |
echo esc_html(sprintf(__('Tax reversed: %1$s', 'fluent-cart'), Helper::toDecimal($reversedTaxAmount))); |
| 46 |
?> |
| 47 |
<?php else : ?> |
| 48 |
<?php echo esc_html(Helper::toDecimal($taxAmount)); ?> |
| 49 |
<?php endif; ?> |
| 50 |
</span> |
| 51 |
</li> |
| 52 |
<?php |
| 53 |
} |
| 54 |
|
| 55 |
public function renderShippingTaxRow($cart, $atts = ''): string |
| 56 |
{ |
| 57 |
if ($this->getCheckoutTaxBreakdownDisplayMode() === 'simplified') { |
| 58 |
return ''; |
| 59 |
} |
| 60 |
|
| 61 |
$shippingTax = (int) Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0); |
| 62 |
$isReverseCharge = $this->module->isReverseChargeCheckout($cart->checkout_data); |
| 63 |
$rcShippingTax = (int) Arr::get($cart->checkout_data, 'tax_data.reverse_charge_shipping_tax', 0); |
| 64 |
|
| 65 |
if ($shippingTax <= 0 && (!$isReverseCharge || $rcShippingTax <= 0)) { |
| 66 |
return ''; |
| 67 |
} |
| 68 |
|
| 69 |
$displayAmount = $isReverseCharge ? $rcShippingTax : $shippingTax; |
| 70 |
$storeBehavior = (int) Arr::get($cart->checkout_data, 'tax_data.store_tax_behavior', |
| 71 |
Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2)); |
| 72 |
$isInclusive = $storeBehavior === 2; |
| 73 |
?> |
| 74 |
<li data-fct-shipping-tax-row <?php echo $atts; ?>> |
| 75 |
<span class="fct_summary_label"> |
| 76 |
<?php if ($isInclusive) : ?> |
| 77 |
<?php echo esc_html__('Shipping Tax (Included)', 'fluent-cart'); ?> |
| 78 |
<?php else : ?> |
| 79 |
<?php echo esc_html__('Shipping Tax (Excluded)', 'fluent-cart'); ?> |
| 80 |
<?php endif; ?> |
| 81 |
</span> |
| 82 |
<span class="fct_summary_value"<?php echo $isReverseCharge ? ' style="text-decoration:line-through;opacity:0.6;"' : ''; ?>> |
| 83 |
<?php echo esc_html(Helper::toDecimal($displayAmount)); ?> |
| 84 |
</span> |
| 85 |
</li> |
| 86 |
<?php |
| 87 |
return ''; |
| 88 |
} |
| 89 |
|
| 90 |
public function renderTaxSummaryBox($cart): void |
| 91 |
{ |
| 92 |
$taxData = Arr::get($cart->checkout_data, 'tax_data', []); |
| 93 |
$taxTotal = (int) Arr::get($taxData, 'tax_total', 0); |
| 94 |
$exclusiveTaxTotal = (int) Arr::get($taxData, 'exclusive_tax_total', $taxTotal); |
| 95 |
$feeTaxLines = (array) Arr::get($taxData, 'fee_tax_lines', []); |
| 96 |
$shippingTax = (int) Arr::get($taxData, 'shipping_tax', 0); |
| 97 |
$isReverseCharge = $this->module->isReverseChargeCheckout($cart->checkout_data); |
| 98 |
$reversedTaxTotalDisplay = (int) Arr::get($taxData, 'reverse_charge_tax_total', 0); |
| 99 |
|
| 100 |
$taxRateLines = (array) Arr::get($taxData, 'tax_lines', []); |
| 101 |
$shippingTaxLines = (array) Arr::get($taxData, 'shipping_tax_lines', []); |
| 102 |
|
| 103 |
$inclusiveFeeTax = 0; |
| 104 |
$exclusiveFeeTax = 0; |
| 105 |
foreach ($feeTaxLines as $feeTaxLine) { |
| 106 |
if (!empty($feeTaxLine['inclusive'])) { |
| 107 |
$inclusiveFeeTax += (int) Arr::get($feeTaxLine, 'tax_amount', 0); |
| 108 |
} else { |
| 109 |
$exclusiveFeeTax += (int) Arr::get($feeTaxLine, 'tax_amount', 0); |
| 110 |
} |
| 111 |
} |
| 112 |
// Fallback: if no fee_tax_lines (old cart data), use aggregate fee_tax as exclusive |
| 113 |
if (empty($feeTaxLines)) { |
| 114 |
$exclusiveFeeTax = (int) Arr::get($taxData, 'fee_tax', 0); |
| 115 |
} |
| 116 |
|
| 117 |
$inclusiveTax = max(0, $taxTotal - $exclusiveTaxTotal - $exclusiveFeeTax - $inclusiveFeeTax); |
| 118 |
$productExclusiveTax = max(0, $exclusiveTaxTotal); |
| 119 |
|
| 120 |
$isShippingInclusive = TaxSummaryHelper::isShippingTaxInclusiveFromTaxData( |
| 121 |
is_array($taxData) ? $taxData : [] |
| 122 |
); |
| 123 |
|
| 124 |
$payableTax = $productExclusiveTax + $exclusiveFeeTax + ($isShippingInclusive ? 0 : $shippingTax); |
| 125 |
$totalOrderTax = $inclusiveTax + $inclusiveFeeTax + ($isShippingInclusive ? $shippingTax : 0) + $payableTax; |
| 126 |
|
| 127 |
$feeRows = TaxSummaryHelper::buildFeeTaxLineRows($feeTaxLines); |
| 128 |
$feeCount = count($feeRows); |
| 129 |
if (empty($feeTaxLines) && (int) Arr::get($taxData, 'fee_tax', 0) > 0) { |
| 130 |
$feeCount = 1; |
| 131 |
} |
| 132 |
$productTaxRowCount = !empty($taxRateLines) |
| 133 |
? count($taxRateLines) |
| 134 |
: (int) ($inclusiveTax > 0) + (int) ($productExclusiveTax > 0); |
| 135 |
$rowCount = $productTaxRowCount + $feeCount + (int) ($shippingTax > 0); |
| 136 |
$shouldShowBreakdown = !empty($taxRateLines) |
| 137 |
|| !empty($shippingTaxLines) |
| 138 |
|| $rowCount >= 2 |
| 139 |
|| ($rowCount === 1 && !($payableTax > 0 || $inclusiveTax > 0 || $inclusiveFeeTax > 0)); |
| 140 |
|
| 141 |
// Build folded per-rate rows via shared helper. Cart tax_lines carry only `label` |
| 142 |
// (no percent), so we pre-augment each line with `rate_label` = "Label (X%)" so the |
| 143 |
// helper picks it up and produces the same display as the old inline block. |
| 144 |
$foldedSource = []; |
| 145 |
foreach ($taxRateLines as $rateKey => $rateLine) { |
| 146 |
$ratePercent = (float) Arr::get($rateLine, 'rate_percent', 0); |
| 147 |
$rateLine['rate_label'] = (string) Arr::get($rateLine, 'label', ''); |
| 148 |
if ($ratePercent > 0) { |
| 149 |
$rateLine['rate_label'] .= ' (' . Helper::formatTaxRatePercent($ratePercent) . '%)'; |
| 150 |
} |
| 151 |
$foldedSource[$rateKey] = $rateLine; |
| 152 |
} |
| 153 |
// Fixed-mode reverse charge leaves tax-inclusive prices (and their embedded VAT) |
| 154 |
// untouched — only dynamic mode reverses the inclusive portion. Exclude inclusive |
| 155 |
// lines from the map in fixed mode so the rate rows sum to the reversed total. |
| 156 |
$rcNonDynamic = $isReverseCharge |
| 157 |
&& Arr::get($taxData, 'reverse_charge_price_mode', 'fixed') !== 'dynamic'; |
| 158 |
$rateBaseMap = TaxSummaryHelper::computeRateBaseMap((array) ($cart->cart_data ?: []), $rcNonDynamic); |
| 159 |
if ($isReverseCharge) { |
| 160 |
// Under reverse charge the stored tax_lines amounts are zeroed at calc time, |
| 161 |
// but the original per-rate amounts survive in item line_meta. Restore them |
| 162 |
// (and the pre-zeroing shipping lines) so the box renders the same |
| 163 |
// breakdown-by-rate table as a normal order. |
| 164 |
foreach ($foldedSource as $rateKey => &$foldedLine) { |
| 165 |
$rid = (int) Arr::get($foldedLine, 'rate_id', $rateKey); |
| 166 |
if (isset($rateBaseMap[$rid])) { |
| 167 |
$foldedLine['tax_amount'] = (int) round($rateBaseMap[$rid]['tax']); |
| 168 |
} |
| 169 |
} |
| 170 |
unset($foldedLine); |
| 171 |
if ($rcNonDynamic) { |
| 172 |
// Rates present only on inclusive lines have nothing reversible in |
| 173 |
// fixed mode — drop them instead of rendering a zero row. |
| 174 |
foreach (array_keys($foldedSource) as $rateKey) { |
| 175 |
$rid = (int) Arr::get($foldedSource[$rateKey], 'rate_id', $rateKey); |
| 176 |
if (!isset($rateBaseMap[$rid])) { |
| 177 |
unset($foldedSource[$rateKey]); |
| 178 |
} |
| 179 |
} |
| 180 |
} |
| 181 |
$shippingTaxLines = (array) Arr::get($taxData, 'reverse_charge_shipping_tax_lines', []); |
| 182 |
} |
| 183 |
$checkoutRateRows = TaxSummaryHelper::buildFoldedRateRows( |
| 184 |
$foldedSource, $shippingTaxLines, 'tax_amount', $isShippingInclusive, $rateBaseMap |
| 185 |
); |
| 186 |
// Inclusive shipping tax follows the store global tax mode — when shipping is |
| 187 |
// priced inclusive its tax is baked into the shipping price, so it counts as |
| 188 |
// "of which included in prices". Keeps includedInPrices + payableTax === totalOrderTax. |
| 189 |
$includedInPrices = $inclusiveTax + $inclusiveFeeTax + ($isShippingInclusive ? $shippingTax : 0); |
| 190 |
|
| 191 |
$displayMode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 192 |
$isSimplified = $displayMode === 'simplified'; |
| 193 |
$simpleLabel = $this->getTaxDisplayLabel(); |
| 194 |
if ($isReverseCharge) { |
| 195 |
$simpleValue = __('Reverse charge', 'fluent-cart'); |
| 196 |
} elseif ($payableTax === 0 && $totalOrderTax > 0) { |
| 197 |
$suffix = (string) Arr::get($this->module->getSettings(), 'price_suffix_included', ''); |
| 198 |
if ($suffix === '') { |
| 199 |
$suffix = __('(incl.)', 'fluent-cart'); |
| 200 |
} |
| 201 |
/* translators: %1$s: formatted tax amount, %2$s: inclusive suffix */ |
| 202 |
$simpleValue = sprintf(__('%1$s %2$s', 'fluent-cart'), html_entity_decode(Helper::toDecimal($totalOrderTax), ENT_QUOTES, 'UTF-8'), $suffix); |
| 203 |
} else { |
| 204 |
$simpleValue = html_entity_decode(Helper::toDecimal($payableTax), ENT_QUOTES, 'UTF-8'); |
| 205 |
} |
| 206 |
|
| 207 |
$tooltipId = 'fct-tax-summary-tooltip-' . Helper::getUidSerial(); |
| 208 |
?> |
| 209 |
<li class="fct_tax_summary_li" data-fct-tax-summary> |
| 210 |
<?php if ($isSimplified) : ?> |
| 211 |
<style> |
| 212 |
.fct_tax_simple_details > summary { list-style: none; cursor: pointer; } |
| 213 |
.fct_tax_simple_details > summary::-webkit-details-marker { display: none; } |
| 214 |
.fct_tax_simple_toggle { color: #2563eb; text-decoration: none; font-size: 11px; } |
| 215 |
.fct_tax_simple_label { color: inherit; } |
| 216 |
</style> |
| 217 |
<details class="fct_tax_simple_details"> |
| 218 |
<summary class="fct_tax_simple_summary" style="display:flex;justify-content:space-between;align-items:center;gap:8px;cursor:pointer;list-style:none;"> |
| 219 |
<span class="fct_tax_simple_left"> |
| 220 |
<span class="fct_tax_simple_label"><?php echo esc_html($simpleLabel); ?></span> |
| 221 |
<span class="fct_tax_simple_toggle"><?php esc_html_e('See details', 'fluent-cart'); ?> ▾</span> |
| 222 |
</span> |
| 223 |
<span class="fct_tax_simple_value"><?php echo esc_html($simpleValue); ?></span> |
| 224 |
</summary> |
| 225 |
<?php endif; ?> |
| 226 |
<div class="fct_tax_summary_box"> |
| 227 |
<div class="fct_tax_summary_header"> |
| 228 |
<span class="fct_tax_summary_heading"> |
| 229 |
<?php echo !empty($checkoutRateRows) |
| 230 |
? esc_html__('Tax breakdown by rate', 'fluent-cart') |
| 231 |
: esc_html__('TAX', 'fluent-cart'); ?> |
| 232 |
</span> |
| 233 |
<div class="fct_item_tax_hint"> |
| 234 |
<button |
| 235 |
type="button" |
| 236 |
class="fct_item_tax_hint_button" |
| 237 |
aria-label="<?php esc_attr_e('Tax information', 'fluent-cart'); ?>" |
| 238 |
aria-describedby="<?php echo esc_attr($tooltipId); ?>" |
| 239 |
> |
| 240 |
<span aria-hidden="true">i</span> |
| 241 |
</button> |
| 242 |
<div class="fct_item_tax_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip"> |
| 243 |
<span class="fct_item_tax_tooltip_heading"> |
| 244 |
<?php esc_html_e('About your tax', 'fluent-cart'); ?> |
| 245 |
</span> |
| 246 |
<?php if ($isReverseCharge) : ?> |
| 247 |
<span class="fct_item_tax_tooltip_line"> |
| 248 |
<?php esc_html_e('Tax has been reversed for this order.', 'fluent-cart'); ?> |
| 249 |
</span> |
| 250 |
<?php else : ?> |
| 251 |
<?php if ($payableTax > 0) : ?> |
| 252 |
<span class="fct_item_tax_tooltip_line"> |
| 253 |
<?php esc_html_e('"Total payable tax" is added on top of listed prices.', 'fluent-cart'); ?> |
| 254 |
</span> |
| 255 |
<?php endif; ?> |
| 256 |
<?php if ($inclusiveTax > 0) : ?> |
| 257 |
<span class="fct_item_tax_tooltip_line"> |
| 258 |
<?php esc_html_e('"Included in item prices" is already built into product prices.', 'fluent-cart'); ?> |
| 259 |
</span> |
| 260 |
<?php endif; ?> |
| 261 |
<?php if ($payableTax === 0 && $inclusiveTax === 0) : ?> |
| 262 |
<span class="fct_item_tax_tooltip_line"> |
| 263 |
<?php esc_html_e('No tax applies to this order.', 'fluent-cart'); ?> |
| 264 |
</span> |
| 265 |
<?php endif; ?> |
| 266 |
<?php endif; ?> |
| 267 |
</div> |
| 268 |
</div> |
| 269 |
</div> |
| 270 |
<div class="fct_tax_summary_rows"> |
| 271 |
<?php if (!empty($checkoutRateRows)) : ?> |
| 272 |
<div class="fct_tax_summary_row fct_tax_summary_row--head" style="display:flex;justify-content:space-between;gap:8px;"> |
| 273 |
<span style="flex:1;font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:#94a3b8;"> |
| 274 |
<?php esc_html_e('Rate', 'fluent-cart'); ?> |
| 275 |
</span> |
| 276 |
<span style="min-width:88px;text-align:right;font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:#94a3b8;"> |
| 277 |
<?php esc_html_e('Taxable base', 'fluent-cart'); ?> |
| 278 |
</span> |
| 279 |
<span style="min-width:64px;text-align:right;font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:#94a3b8;"> |
| 280 |
<?php esc_html_e('Tax', 'fluent-cart'); ?> |
| 281 |
</span> |
| 282 |
</div> |
| 283 |
<?php foreach ($checkoutRateRows as $checkoutRateRow) : ?> |
| 284 |
<div class="fct_tax_summary_row<?php echo !empty($checkoutRateRow['inclusive']) ? ' fct_tax_summary_row--muted' : ''; ?>" style="display:flex;justify-content:space-between;gap:8px;"> |
| 285 |
<span class="fct_tax_summary_row_label" style="flex:1;"> |
| 286 |
<?php echo esc_html($checkoutRateRow['label']); ?> |
| 287 |
</span> |
| 288 |
<span style="min-width:88px;text-align:right;color:#94a3b8;"> |
| 289 |
<?php echo esc_html(Helper::toDecimal((int) $checkoutRateRow['base'])); ?> |
| 290 |
</span> |
| 291 |
<span class="fct_tax_summary_row_amount" style="min-width:64px;text-align:right;"> |
| 292 |
<?php echo esc_html(Helper::toDecimal((int) $checkoutRateRow['tax'])); ?> |
| 293 |
</span> |
| 294 |
</div> |
| 295 |
<?php endforeach; ?> |
| 296 |
<?php endif; ?> |
| 297 |
<?php if ($isReverseCharge) : ?> |
| 298 |
<?php if (empty($checkoutRateRows)) : ?> |
| 299 |
<?php |
| 300 |
$rcShippingDisplay = (int) Arr::get($taxData, 'reverse_charge_shipping_tax', 0); |
| 301 |
$rcInclusiveAdj = (int) Arr::get($taxData, 'reverse_charge_inclusive_adjustment', 0); |
| 302 |
$rcExclusiveNonShip = max(0, $reversedTaxTotalDisplay - $rcShippingDisplay - $rcInclusiveAdj); |
| 303 |
$rcBreakdownCount = (int) ($rcInclusiveAdj > 0) + (int) ($rcExclusiveNonShip > 0) + (int) ($rcShippingDisplay > 0); |
| 304 |
?> |
| 305 |
<?php if ($rcBreakdownCount >= 2) : ?> |
| 306 |
<?php if ($rcInclusiveAdj > 0) : ?> |
| 307 |
<div class="fct_tax_summary_row fct_tax_summary_row--muted"> |
| 308 |
<span class="fct_tax_summary_row_label"> |
| 309 |
<?php esc_html_e('Included in item prices', 'fluent-cart'); ?> |
| 310 |
</span> |
| 311 |
<span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;"> |
| 312 |
<?php echo esc_html(Helper::toDecimal($rcInclusiveAdj)); ?> |
| 313 |
</span> |
| 314 |
</div> |
| 315 |
<?php endif; ?> |
| 316 |
<?php if ($rcExclusiveNonShip > 0) : ?> |
| 317 |
<div class="fct_tax_summary_row"> |
| 318 |
<span class="fct_tax_summary_row_label"> |
| 319 |
<?php esc_html_e('Added on products', 'fluent-cart'); ?> |
| 320 |
</span> |
| 321 |
<span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;"> |
| 322 |
<?php echo esc_html(Helper::toDecimal($rcExclusiveNonShip)); ?> |
| 323 |
</span> |
| 324 |
</div> |
| 325 |
<?php endif; ?> |
| 326 |
<?php if ($rcShippingDisplay > 0) : ?> |
| 327 |
<div class="fct_tax_summary_row<?php echo $isShippingInclusive ? ' fct_tax_summary_row--muted' : ''; ?>"> |
| 328 |
<span class="fct_tax_summary_row_label"> |
| 329 |
<?php echo $isShippingInclusive ? esc_html__('Included in shipping prices', 'fluent-cart') : esc_html__('Added on shipping', 'fluent-cart'); ?> |
| 330 |
</span> |
| 331 |
<span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;"> |
| 332 |
<?php echo esc_html(Helper::toDecimal($rcShippingDisplay)); ?> |
| 333 |
</span> |
| 334 |
</div> |
| 335 |
<?php endif; ?> |
| 336 |
<?php elseif ($rcShippingDisplay > 0) : ?> |
| 337 |
<div class="fct_tax_summary_row fct_tax_summary_row--muted"> |
| 338 |
<span class="fct_tax_summary_row_label"> |
| 339 |
<?php echo $isShippingInclusive ? esc_html__('Included in shipping prices', 'fluent-cart') : esc_html__('Added on shipping', 'fluent-cart'); ?> |
| 340 |
</span> |
| 341 |
<span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;"> |
| 342 |
<?php echo esc_html(Helper::toDecimal($rcShippingDisplay)); ?> |
| 343 |
</span> |
| 344 |
</div> |
| 345 |
<?php endif; ?> |
| 346 |
<?php endif; ?> |
| 347 |
<div class="fct_tax_summary_row fct_tax_summary_row--total"> |
| 348 |
<span class="fct_tax_summary_row_label"> |
| 349 |
<?php esc_html_e('VAT reversed', 'fluent-cart'); ?> |
| 350 |
</span> |
| 351 |
<span class="fct_tax_summary_row_amount"> |
| 352 |
<?php echo esc_html(Helper::toDecimal($reversedTaxTotalDisplay)); ?> |
| 353 |
</span> |
| 354 |
</div> |
| 355 |
<?php else : ?> |
| 356 |
<?php if (empty($checkoutRateRows) && $inclusiveTax > 0 && $shouldShowBreakdown) : ?> |
| 357 |
<div class="fct_tax_summary_row fct_tax_summary_row--muted"> |
| 358 |
<span class="fct_tax_summary_row_label"> |
| 359 |
<?php esc_html_e('Included in item prices', 'fluent-cart'); ?> |
| 360 |
</span> |
| 361 |
<span class="fct_tax_summary_row_amount"> |
| 362 |
<?php echo esc_html(Helper::toDecimal($inclusiveTax)); ?> |
| 363 |
</span> |
| 364 |
</div> |
| 365 |
<?php endif; ?> |
| 366 |
<?php if (empty($checkoutRateRows) && $productExclusiveTax > 0 && $shouldShowBreakdown) : ?> |
| 367 |
<div class="fct_tax_summary_row"> |
| 368 |
<span class="fct_tax_summary_row_label"> |
| 369 |
<?php esc_html_e('Added on products', 'fluent-cart'); ?> |
| 370 |
</span> |
| 371 |
<span class="fct_tax_summary_row_amount"> |
| 372 |
<?php echo esc_html(Helper::toDecimal($productExclusiveTax)); ?> |
| 373 |
</span> |
| 374 |
</div> |
| 375 |
<?php endif; ?> |
| 376 |
<?php if (empty($checkoutRateRows) && $shouldShowBreakdown) : ?> |
| 377 |
<?php foreach ($feeRows as $feeRow) : ?> |
| 378 |
<div class="fct_tax_summary_row<?php echo $feeRow['inclusive'] ? ' fct_tax_summary_row--muted' : ''; ?>"> |
| 379 |
<span class="fct_tax_summary_row_label"> |
| 380 |
<?php echo esc_html($feeRow['display_label']); ?> |
| 381 |
</span> |
| 382 |
<span class="fct_tax_summary_row_amount"> |
| 383 |
<?php echo esc_html(Helper::toDecimal($feeRow['tax_amount'])); ?> |
| 384 |
</span> |
| 385 |
</div> |
| 386 |
<?php endforeach; ?> |
| 387 |
<?php endif; ?> |
| 388 |
<?php if (empty($checkoutRateRows) && empty($feeTaxLines) && (int) Arr::get($taxData, 'fee_tax', 0) > 0 && $shouldShowBreakdown) : ?> |
| 389 |
<div class="fct_tax_summary_row"> |
| 390 |
<span class="fct_tax_summary_row_label"> |
| 391 |
<?php esc_html_e('Added on fees', 'fluent-cart'); ?> |
| 392 |
</span> |
| 393 |
<span class="fct_tax_summary_row_amount"> |
| 394 |
<?php echo esc_html(Helper::toDecimal((int) Arr::get($taxData, 'fee_tax', 0))); ?> |
| 395 |
</span> |
| 396 |
</div> |
| 397 |
<?php endif; ?> |
| 398 |
<?php if (empty($checkoutRateRows) && $shippingTax > 0 && $shouldShowBreakdown) : ?> |
| 399 |
<?php if (!empty($shippingTaxLines)) : ?> |
| 400 |
<?php foreach ($shippingTaxLines as $shippingTaxLine) : ?> |
| 401 |
<div class="fct_tax_summary_row<?php echo $isShippingInclusive ? ' fct_tax_summary_row--muted' : ''; ?>"> |
| 402 |
<span class="fct_tax_summary_row_label"> |
| 403 |
<?php echo esc_html($shippingTaxLine['label']); ?> |
| 404 |
</span> |
| 405 |
<span class="fct_tax_summary_row_amount"> |
| 406 |
<?php echo esc_html(Helper::toDecimal((int) Arr::get($shippingTaxLine, 'shipping_tax', 0))); ?> |
| 407 |
</span> |
| 408 |
</div> |
| 409 |
<?php endforeach; ?> |
| 410 |
<?php else : ?> |
| 411 |
<div class="fct_tax_summary_row<?php echo $isShippingInclusive ? ' fct_tax_summary_row--muted' : ''; ?>"> |
| 412 |
<span class="fct_tax_summary_row_label"> |
| 413 |
<?php if ($isShippingInclusive) : ?> |
| 414 |
<?php esc_html_e('Included in shipping prices', 'fluent-cart'); ?> |
| 415 |
<?php else : ?> |
| 416 |
<?php esc_html_e('Added on shipping', 'fluent-cart'); ?> |
| 417 |
<?php endif; ?> |
| 418 |
</span> |
| 419 |
<span class="fct_tax_summary_row_amount"> |
| 420 |
<?php echo esc_html(Helper::toDecimal($shippingTax)); ?> |
| 421 |
</span> |
| 422 |
</div> |
| 423 |
<?php endif; ?> |
| 424 |
<?php endif; ?> |
| 425 |
<?php if (!empty($checkoutRateRows)) : ?> |
| 426 |
<div class="fct_tax_summary_row fct_tax_summary_row--total"> |
| 427 |
<span class="fct_tax_summary_row_label"><?php esc_html_e('Total tax', 'fluent-cart'); ?></span> |
| 428 |
<span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($totalOrderTax)); ?></span> |
| 429 |
</div> |
| 430 |
<?php if ($includedInPrices > 0) : ?> |
| 431 |
<div class="fct_tax_summary_row fct_tax_summary_row--muted"> |
| 432 |
<span class="fct_tax_summary_row_label"><?php esc_html_e('of which included in prices', 'fluent-cart'); ?></span> |
| 433 |
<span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($includedInPrices)); ?></span> |
| 434 |
</div> |
| 435 |
<?php endif; ?> |
| 436 |
<?php if ($payableTax > 0 && $includedInPrices > 0) : ?> |
| 437 |
<div class="fct_tax_summary_row fct_tax_summary_row--total"> |
| 438 |
<span class="fct_tax_summary_row_label"><?php esc_html_e('Payable now (added)', 'fluent-cart'); ?></span> |
| 439 |
<span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($payableTax)); ?></span> |
| 440 |
</div> |
| 441 |
<?php endif; ?> |
| 442 |
<?php else : ?> |
| 443 |
<?php if ($payableTax > 0) : ?> |
| 444 |
<div class="fct_tax_summary_row fct_tax_summary_row--total"> |
| 445 |
<span class="fct_tax_summary_row_label"><?php esc_html_e('Total payable tax', 'fluent-cart'); ?></span> |
| 446 |
<span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($payableTax)); ?></span> |
| 447 |
</div> |
| 448 |
<?php endif; ?> |
| 449 |
<?php if ($inclusiveTax > 0 || $inclusiveFeeTax > 0) : ?> |
| 450 |
<div class="fct_tax_summary_row fct_tax_summary_row--muted"> |
| 451 |
<span class="fct_tax_summary_row_label"><?php esc_html_e('Total tax in this order', 'fluent-cart'); ?></span> |
| 452 |
<span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($totalOrderTax)); ?></span> |
| 453 |
</div> |
| 454 |
<?php endif; ?> |
| 455 |
<?php endif; ?> |
| 456 |
<?php endif; ?> |
| 457 |
</div> |
| 458 |
</div> |
| 459 |
<?php if ($isSimplified) : ?> |
| 460 |
</details> |
| 461 |
<?php endif; ?> |
| 462 |
</li> |
| 463 |
<?php |
| 464 |
} |
| 465 |
|
| 466 |
public function renderCheckoutLineItemTaxLabel($data): void |
| 467 |
{ |
| 468 |
$mode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 469 |
if ($mode === 'simplified') { |
| 470 |
return; |
| 471 |
} |
| 472 |
|
| 473 |
$item = Arr::get($data, 'item', []); |
| 474 |
$rates = $this->normalizeCheckoutLineTaxRates($item); |
| 475 |
if (empty($rates)) { |
| 476 |
return; |
| 477 |
} |
| 478 |
$cart = CartHelper::getCart(); |
| 479 |
$isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; |
| 480 |
$rcMode = $this->module->getEffectiveRcMode(); |
| 481 |
$this->renderTaxBadges($rates, $isReversed, $rcMode); |
| 482 |
} |
| 483 |
|
| 484 |
public function renderCheckoutSetupFeeTaxLabel($data): void |
| 485 |
{ |
| 486 |
$mode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 487 |
if ($mode === 'simplified') { |
| 488 |
return; |
| 489 |
} |
| 490 |
|
| 491 |
$item = Arr::get($data, 'item', []); |
| 492 |
$signupFeeTaxConfig = Arr::get($item, 'signup_fee_tax_config', []); |
| 493 |
$rates = Arr::get($signupFeeTaxConfig, 'rates', []); |
| 494 |
if (empty($rates) || !is_array($rates)) { |
| 495 |
return; |
| 496 |
} |
| 497 |
|
| 498 |
$isInclusive = (bool) Arr::get($signupFeeTaxConfig, 'inclusive', false); |
| 499 |
$normalizedRates = []; |
| 500 |
foreach ($rates as $rate) { |
| 501 |
$taxAmount = (int) Arr::get($rate, 'tax_amount', 0); |
| 502 |
$ratePercent = (float) Arr::get($rate, 'rate_percent', 0); |
| 503 |
if ($taxAmount <= 0 && $ratePercent <= 0) { |
| 504 |
continue; |
| 505 |
} |
| 506 |
$normalizedRates[] = [ |
| 507 |
'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')), |
| 508 |
'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent), |
| 509 |
'inclusive' => $isInclusive, |
| 510 |
'tax_amount' => $taxAmount, |
| 511 |
]; |
| 512 |
} |
| 513 |
|
| 514 |
if (empty($normalizedRates)) { |
| 515 |
return; |
| 516 |
} |
| 517 |
$cart = CartHelper::getCart(); |
| 518 |
$isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; |
| 519 |
$rcMode = $this->module->getEffectiveRcMode(); |
| 520 |
$this->renderTaxBadges($normalizedRates, $isReversed, $rcMode); |
| 521 |
} |
| 522 |
|
| 523 |
private function renderTaxBadges(array $normalizedRates, $isReversed = false, $rcMode = 'fixed'): void |
| 524 |
{ |
| 525 |
// Fixed-mode reverse charge: inclusive-priced lines keep their gross price and |
| 526 |
// no VAT is charged, so an "incl. X" badge would be misleading — hide those rates. |
| 527 |
if ($isReversed && $rcMode !== 'dynamic') { |
| 528 |
$normalizedRates = array_values(array_filter($normalizedRates, function ($rate) { |
| 529 |
return empty($rate['inclusive']); |
| 530 |
})); |
| 531 |
} |
| 532 |
if (empty($normalizedRates)) { |
| 533 |
return; |
| 534 |
} |
| 535 |
?> |
| 536 |
<div class="fct_item_tax_badges" aria-label="<?php esc_attr_e('Tax breakdown', 'fluent-cart'); ?>"> |
| 537 |
<?php foreach ($normalizedRates as $rate) : ?> |
| 538 |
<div class="fct_item_tax_badge_row"> |
| 539 |
<span class="fct_item_tax_badge <?php echo esc_attr($rate['inclusive'] ? 'is-inclusive' : 'is-exclusive'); ?>"> |
| 540 |
<span> |
| 541 |
<?php |
| 542 |
$badgeText = sprintf( |
| 543 |
/* translators: %1$s: tax label, %2$s: tax rate percent */ |
| 544 |
__('%1$s (%2$s%%)', 'fluent-cart'), |
| 545 |
$rate['short_label'], |
| 546 |
$rate['formatted_rate'] |
| 547 |
); |
| 548 |
echo esc_html($badgeText); |
| 549 |
?> |
| 550 |
</span> |
| 551 |
</span> |
| 552 |
<?php if (!empty($rate['tax_amount']) && (int) $rate['tax_amount'] > 0) : ?> |
| 553 |
<?php $rateReversedClass = $this->shouldRateStrikethrough($isReversed, $rate['inclusive'], $rcMode) ? ' is-reversed' : ''; ?> |
| 554 |
<span class="fct_item_tax_badge_amount <?php echo esc_attr($rate['inclusive'] ? 'is-inclusive' : 'is-exclusive'); ?><?php echo esc_attr($rateReversedClass); ?>"> |
| 555 |
<?php |
| 556 |
$amountText = $rate['inclusive'] |
| 557 |
? sprintf( |
| 558 |
/* translators: %1$s: formatted tax amount */ |
| 559 |
__('incl. %1$s', 'fluent-cart'), |
| 560 |
Helper::toDecimal((int) $rate['tax_amount']) |
| 561 |
) |
| 562 |
: sprintf( |
| 563 |
/* translators: %1$s: formatted tax amount */ |
| 564 |
__('+ %1$s', 'fluent-cart'), |
| 565 |
Helper::toDecimal((int) $rate['tax_amount']) |
| 566 |
); |
| 567 |
echo esc_html($amountText); |
| 568 |
?> |
| 569 |
</span> |
| 570 |
<?php endif; ?> |
| 571 |
</div> |
| 572 |
<?php endforeach; ?> |
| 573 |
</div> |
| 574 |
<?php |
| 575 |
} |
| 576 |
|
| 577 |
private function getSetupFeeTaxData($item): ?array |
| 578 |
{ |
| 579 |
$signupFeeTaxConfig = Arr::get($item, 'signup_fee_tax_config', []); |
| 580 |
$rawRates = Arr::get($signupFeeTaxConfig, 'rates', []); |
| 581 |
if (empty($rawRates) || !is_array($rawRates)) { |
| 582 |
return null; |
| 583 |
} |
| 584 |
|
| 585 |
$isInclusive = (bool) Arr::get($signupFeeTaxConfig, 'inclusive', false); |
| 586 |
$rates = []; |
| 587 |
foreach ($rawRates as $rate) { |
| 588 |
$taxAmount = (int) Arr::get($rate, 'tax_amount', 0); |
| 589 |
$ratePercent = (float) Arr::get($rate, 'rate_percent', 0); |
| 590 |
if ($taxAmount <= 0 && $ratePercent <= 0) { |
| 591 |
continue; |
| 592 |
} |
| 593 |
$taxableAmount = (int) Arr::get($rate, 'taxable_amount', 0); |
| 594 |
$rates[] = [ |
| 595 |
'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')), |
| 596 |
'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent), |
| 597 |
'tax_amount' => $taxAmount, |
| 598 |
'display_base' => $isInclusive ? max(0, $taxableAmount - $taxAmount) : $taxableAmount, |
| 599 |
]; |
| 600 |
} |
| 601 |
|
| 602 |
if (empty($rates)) { |
| 603 |
return null; |
| 604 |
} |
| 605 |
|
| 606 |
$setupFee = (int) Arr::get($item, 'other_info.signup_fee', 0); |
| 607 |
$totalTax = array_sum(array_map(function ($rate) { |
| 608 |
return (int) $rate['tax_amount']; |
| 609 |
}, $rates)); |
| 610 |
|
| 611 |
return [ |
| 612 |
'rates' => $rates, |
| 613 |
'is_inclusive' => $isInclusive, |
| 614 |
'total_tax' => $totalTax, |
| 615 |
'display_total' => $isInclusive ? $setupFee : $setupFee + $totalTax, |
| 616 |
'primary_label' => Arr::get($rates, '0.short_label', __('Tax', 'fluent-cart')), |
| 617 |
]; |
| 618 |
} |
| 619 |
|
| 620 |
public function renderCheckoutSetupFeeTaxTooltip($data): void |
| 621 |
{ |
| 622 |
$mode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 623 |
if ($mode === 'simplified') { |
| 624 |
return; |
| 625 |
} |
| 626 |
|
| 627 |
$item = Arr::get($data, 'item', []); |
| 628 |
$taxData = $this->getSetupFeeTaxData($item); |
| 629 |
if (!$taxData) { |
| 630 |
return; |
| 631 |
} |
| 632 |
|
| 633 |
$cart = CartHelper::getCart(); |
| 634 |
$isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; |
| 635 |
$rcMode = $this->module->getEffectiveRcMode(); |
| 636 |
|
| 637 |
$rates = $taxData['rates']; |
| 638 |
$isInclusive = $taxData['is_inclusive']; |
| 639 |
$displayTotal = $taxData['display_total']; |
| 640 |
if ($isReversed) { |
| 641 |
if ($taxData['is_inclusive'] && $rcMode === 'dynamic') { |
| 642 |
$displayTotal = $taxData['display_total'] - $taxData['total_tax']; |
| 643 |
} else { |
| 644 |
$displayTotal = (int) Arr::get($data, 'item.other_info.signup_fee', 0); |
| 645 |
} |
| 646 |
} |
| 647 |
$tooltipId = 'fct-item-tax-tooltip-' . Helper::getUidSerial(); |
| 648 |
?> |
| 649 |
<div class="fct_item_tax_hint"> |
| 650 |
<button |
| 651 |
type="button" |
| 652 |
class="fct_item_tax_hint_button" |
| 653 |
aria-label="<?php esc_attr_e('View tax breakdown for this item', 'fluent-cart'); ?>" |
| 654 |
aria-describedby="<?php echo esc_attr($tooltipId); ?>" |
| 655 |
> |
| 656 |
<span aria-hidden="true">i</span> |
| 657 |
</button> |
| 658 |
<div class="fct_item_tax_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip"> |
| 659 |
<span class="fct_item_tax_tooltip_heading"> |
| 660 |
<?php echo esc_html($isInclusive ? __('Tax-inclusive price', 'fluent-cart') : __('Tax-exclusive price', 'fluent-cart')); ?> |
| 661 |
</span> |
| 662 |
<?php foreach ($rates as $rate) : ?> |
| 663 |
<?php $lineReversedClass = $this->shouldRateStrikethrough($isReversed, isset($rate['inclusive']) ? (bool)$rate['inclusive'] : $taxData['is_inclusive'], $rcMode) ? ' is-reversed' : ''; ?> |
| 664 |
<span class="fct_item_tax_tooltip_line<?php echo esc_attr($lineReversedClass); ?>"> |
| 665 |
<?php |
| 666 |
$lineText = sprintf( |
| 667 |
/* translators: %1$s: tax base amount, %2$s: tax label, %3$s: tax rate percent, %4$s: tax amount */ |
| 668 |
__('Base %1$s + %2$s %3$s%% %4$s', 'fluent-cart'), |
| 669 |
Helper::toDecimal($rate['display_base']), |
| 670 |
$rate['short_label'], |
| 671 |
$rate['formatted_rate'], |
| 672 |
Helper::toDecimal($rate['tax_amount']) |
| 673 |
); |
| 674 |
echo esc_html($lineText); |
| 675 |
?> |
| 676 |
</span> |
| 677 |
<?php endforeach; ?> |
| 678 |
<span class="fct_item_tax_tooltip_line is-total"> |
| 679 |
<?php |
| 680 |
echo esc_html(sprintf( |
| 681 |
/* translators: %1$s: line total amount */ |
| 682 |
__('Total %1$s', 'fluent-cart'), |
| 683 |
Helper::toDecimal($displayTotal) |
| 684 |
)); |
| 685 |
?> |
| 686 |
</span> |
| 687 |
</div> |
| 688 |
</div> |
| 689 |
<?php |
| 690 |
} |
| 691 |
|
| 692 |
public function renderCheckoutSetupFeeTaxInfo($data): void |
| 693 |
{ |
| 694 |
$mode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 695 |
if ($mode === 'simplified') { |
| 696 |
return; |
| 697 |
} |
| 698 |
|
| 699 |
$item = Arr::get($data, 'item', []); |
| 700 |
$taxData = $this->getSetupFeeTaxData($item); |
| 701 |
if (!$taxData) { |
| 702 |
return; |
| 703 |
} |
| 704 |
|
| 705 |
$isInclusive = $taxData['is_inclusive']; |
| 706 |
$totalTax = $taxData['total_tax']; |
| 707 |
$primaryLabel = $taxData['primary_label']; |
| 708 |
|
| 709 |
$priceNote = $isInclusive |
| 710 |
? sprintf( |
| 711 |
/* translators: %1$s: tax label */ |
| 712 |
__('%1$s incl.', 'fluent-cart'), |
| 713 |
$primaryLabel |
| 714 |
) |
| 715 |
: sprintf( |
| 716 |
/* translators: %1$s: tax amount, %2$s: tax label */ |
| 717 |
__('+ %1$s %2$s', 'fluent-cart'), |
| 718 |
Helper::toDecimal($totalTax), |
| 719 |
strtolower($primaryLabel) |
| 720 |
); |
| 721 |
?> |
| 722 |
<span class="fct_setup_fee_price_note"><?php echo esc_html($priceNote); ?></span> |
| 723 |
<?php |
| 724 |
} |
| 725 |
|
| 726 |
public function renderCheckoutLineItemTaxTooltip($data): void |
| 727 |
{ |
| 728 |
$mode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 729 |
if ($mode === 'simplified') { |
| 730 |
return; |
| 731 |
} |
| 732 |
|
| 733 |
$item = Arr::get($data, 'item', []); |
| 734 |
$rates = $this->normalizeCheckoutLineTaxRates($item); |
| 735 |
if (empty($rates)) { |
| 736 |
return; |
| 737 |
} |
| 738 |
|
| 739 |
$cart = CartHelper::getCart(); |
| 740 |
$isReversed = $cart ? $this->module->isReverseChargeCheckout($cart->checkout_data) : false; |
| 741 |
$rcMode = $this->module->getEffectiveRcMode(); |
| 742 |
|
| 743 |
$isInclusive = (bool) Arr::get($rates, '0.inclusive', false); |
| 744 |
$itemSubtotal = (int) Arr::get($item, 'line_total', Arr::get($item, 'subtotal', 0)); |
| 745 |
$itemTaxAmount = array_sum(array_map(function ($rate) { |
| 746 |
return (int) Arr::get($rate, 'tax_amount', 0); |
| 747 |
}, $rates)); |
| 748 |
if ($isReversed) { |
| 749 |
if ($isInclusive && $rcMode === 'dynamic') { |
| 750 |
$displayTotal = $itemSubtotal - $itemTaxAmount; |
| 751 |
} else { |
| 752 |
$displayTotal = $itemSubtotal; |
| 753 |
} |
| 754 |
} else { |
| 755 |
$displayTotal = $isInclusive ? $itemSubtotal : $itemSubtotal + $itemTaxAmount; |
| 756 |
} |
| 757 |
$tooltipId = 'fct-item-tax-tooltip-' . Helper::getUidSerial(); |
| 758 |
?> |
| 759 |
<div class="fct_item_tax_hint"> |
| 760 |
<button |
| 761 |
type="button" |
| 762 |
class="fct_item_tax_hint_button" |
| 763 |
aria-label="<?php esc_attr_e('View tax breakdown for this item', 'fluent-cart'); ?>" |
| 764 |
aria-describedby="<?php echo esc_attr($tooltipId); ?>" |
| 765 |
> |
| 766 |
<span aria-hidden="true">i</span> |
| 767 |
</button> |
| 768 |
<div class="fct_item_tax_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip"> |
| 769 |
<span class="fct_item_tax_tooltip_heading"> |
| 770 |
<?php echo esc_html($isInclusive ? __('Tax-inclusive price', 'fluent-cart') : __('Tax-exclusive price', 'fluent-cart')); ?> |
| 771 |
</span> |
| 772 |
<?php foreach ($rates as $rate) : ?> |
| 773 |
<?php $lineReversedClass = $this->shouldRateStrikethrough($isReversed, $rate['inclusive'], $rcMode) ? ' is-reversed' : ''; ?> |
| 774 |
<span class="fct_item_tax_tooltip_line<?php echo esc_attr($lineReversedClass); ?>"> |
| 775 |
<?php |
| 776 |
$lineText = sprintf( |
| 777 |
/* translators: %1$s: tax base amount, %2$s: tax label, %3$s: tax rate percent, %4$s: tax amount */ |
| 778 |
__('Base %1$s + %2$s %3$s%% %4$s', 'fluent-cart'), |
| 779 |
Helper::toDecimal($rate['display_base']), |
| 780 |
$rate['short_label'], |
| 781 |
$rate['formatted_rate'], |
| 782 |
Helper::toDecimal($rate['tax_amount']) |
| 783 |
); |
| 784 |
echo esc_html($lineText); |
| 785 |
?> |
| 786 |
</span> |
| 787 |
<?php endforeach; ?> |
| 788 |
<span class="fct_item_tax_tooltip_line is-total"> |
| 789 |
<?php |
| 790 |
echo esc_html(sprintf( |
| 791 |
/* translators: %1$s: line total amount */ |
| 792 |
__('Total %1$s', 'fluent-cart'), |
| 793 |
Helper::toDecimal($displayTotal) |
| 794 |
)); |
| 795 |
?> |
| 796 |
</span> |
| 797 |
</div> |
| 798 |
</div> |
| 799 |
<?php |
| 800 |
} |
| 801 |
|
| 802 |
public function renderCheckoutLineItemTaxInfo($data): void |
| 803 |
{ |
| 804 |
$mode = $this->getCheckoutTaxBreakdownDisplayMode(); |
| 805 |
if ($mode === 'simplified') { |
| 806 |
return; |
| 807 |
} |
| 808 |
|
| 809 |
$item = Arr::get($data, 'item', []); |
| 810 |
$rates = $this->normalizeCheckoutLineTaxRates($item); |
| 811 |
if (empty($rates)) { |
| 812 |
return; |
| 813 |
} |
| 814 |
|
| 815 |
$isInclusive = (bool) Arr::get($rates, '0.inclusive', false); |
| 816 |
$itemTaxAmount = array_sum(array_map(function ($rate) { |
| 817 |
return (int) Arr::get($rate, 'tax_amount', 0); |
| 818 |
}, $rates)); |
| 819 |
$primaryLabel = Arr::get($rates, '0.short_label', __('Tax', 'fluent-cart')); |
| 820 |
|
| 821 |
$priceNote = $isInclusive |
| 822 |
? sprintf( |
| 823 |
/* translators: %1$s: tax label */ |
| 824 |
__('%1$s incl.', 'fluent-cart'), |
| 825 |
$primaryLabel |
| 826 |
) |
| 827 |
: sprintf( |
| 828 |
/* translators: %1$s: tax amount, %2$s: tax label */ |
| 829 |
__('+ %1$s %2$s', 'fluent-cart'), |
| 830 |
Helper::toDecimal($itemTaxAmount), |
| 831 |
strtolower($primaryLabel) |
| 832 |
); |
| 833 |
?> |
| 834 |
<div class="fct_item_tax_price_note"><?php echo esc_html($priceNote); ?></div> |
| 835 |
<?php |
| 836 |
} |
| 837 |
|
| 838 |
public function renderUnitPriceRoundingTooltip($data): void |
| 839 |
{ |
| 840 |
$item = Arr::get($data, 'item', []); |
| 841 |
$quantity = (int) Arr::get($item, 'quantity', 1); |
| 842 |
|
| 843 |
if ($quantity < 2) { |
| 844 |
return; |
| 845 |
} |
| 846 |
|
| 847 |
$unitPrice = (int) Arr::get($item, 'unit_price', 0); |
| 848 |
$subtotal = (int) Arr::get($item, 'subtotal', 0); |
| 849 |
|
| 850 |
if ($unitPrice <= 0) { |
| 851 |
return; |
| 852 |
} |
| 853 |
|
| 854 |
$shouldShow = false; |
| 855 |
|
| 856 |
// Case 1 — TaxModule RC dynamic: the applied per-unit tax adjustment is rounded, |
| 857 |
// so unit_price * qty may differ from the exact net by up to (qty - 1) cents. |
| 858 |
$rcAdjustment = (int) Arr::get($item, 'line_meta.reverse_charge_adjustment', 0); |
| 859 |
if ($rcAdjustment > 0) { |
| 860 |
$rates = (array) Arr::get($item, 'line_meta.tax_config.rates', []); |
| 861 |
$actualTaxTotal = (int) array_sum(array_map('intval', array_column($rates, 'tax_amount'))); |
| 862 |
$diff = abs($rcAdjustment - $actualTaxTotal); |
| 863 |
if ($diff > 0 && $diff <= $quantity) { |
| 864 |
$shouldShow = true; |
| 865 |
} |
| 866 |
} |
| 867 |
|
| 868 |
// Case 2 — generic: unit_price * qty already doesn't match subtotal |
| 869 |
// (future dynamic pricing or any other module that stores a rounded unit_price) |
| 870 |
if (!$shouldShow && $subtotal > 0) { |
| 871 |
$diff = abs(($unitPrice * $quantity) - $subtotal); |
| 872 |
if ($diff > 0 && $diff <= $quantity) { |
| 873 |
$shouldShow = true; |
| 874 |
} |
| 875 |
} |
| 876 |
|
| 877 |
if (!$shouldShow) { |
| 878 |
return; |
| 879 |
} |
| 880 |
|
| 881 |
$tooltipId = 'fct-unit-price-rounding-' . Helper::getUidSerial(); |
| 882 |
?> |
| 883 |
<div class="fct_item_tax_hint"> |
| 884 |
<button |
| 885 |
type="button" |
| 886 |
class="fct_item_tax_hint_button" |
| 887 |
aria-label="<?php esc_attr_e('Unit price rounding information', 'fluent-cart'); ?>" |
| 888 |
aria-describedby="<?php echo esc_attr($tooltipId); ?>" |
| 889 |
> |
| 890 |
<span aria-hidden="true">i</span> |
| 891 |
</button> |
| 892 |
<div class="fct_item_tax_tooltip fct_unit_price_rounding_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip"> |
| 893 |
<?php esc_html_e('Unit price is rounded for display. The line total is calculated at full precision, so it always reconciles exactly.', 'fluent-cart'); ?> |
| 894 |
</div> |
| 895 |
</div> |
| 896 |
<?php |
| 897 |
} |
| 898 |
|
| 899 |
protected function getCheckoutTaxBreakdownDisplayMode(): string |
| 900 |
{ |
| 901 |
// Backward compat: legacy stored values ('both', 'label', 'tooltip', or anything |
| 902 |
// else) all collapse to 'itemized'. Only an explicit 'simplified' stays simplified. |
| 903 |
$mode = Arr::get($this->module->getSettings(), 'checkout_tax_breakdown_display', 'itemized'); |
| 904 |
|
| 905 |
return $mode === 'simplified' ? 'simplified' : 'itemized'; |
| 906 |
} |
| 907 |
|
| 908 |
protected function getTaxDisplayLabel(): string |
| 909 |
{ |
| 910 |
$label = trim((string) Arr::get($this->module->getSettings(), 'tax_display_label', '')); |
| 911 |
return $label !== '' ? $label : __('Tax', 'fluent-cart'); |
| 912 |
} |
| 913 |
|
| 914 |
protected function normalizeCheckoutLineTaxRates($item): array |
| 915 |
{ |
| 916 |
$rates = Arr::get($item, 'line_meta.tax_config.rates', []); |
| 917 |
if (empty($rates) || !is_array($rates)) { |
| 918 |
return []; |
| 919 |
} |
| 920 |
|
| 921 |
$isInclusive = (bool) Arr::get($item, 'line_meta.tax_config.inclusive', false); |
| 922 |
$normalizedRates = []; |
| 923 |
|
| 924 |
foreach ($rates as $rate) { |
| 925 |
$taxAmount = (int) Arr::get($rate, 'tax_amount', 0); |
| 926 |
$ratePercent = (float) Arr::get($rate, 'rate_percent', 0); |
| 927 |
$taxableAmount = (int) Arr::get($rate, 'taxable_amount', 0); |
| 928 |
|
| 929 |
if ($taxAmount <= 0 && $ratePercent <= 0) { |
| 930 |
continue; |
| 931 |
} |
| 932 |
|
| 933 |
$normalizedRates[] = [ |
| 934 |
'label' => (string) Arr::get($rate, 'label', __('Tax', 'fluent-cart')), |
| 935 |
'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')), |
| 936 |
'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent), |
| 937 |
'tax_amount' => $taxAmount, |
| 938 |
'display_base' => $isInclusive ? max(0, $taxableAmount - $taxAmount) : $taxableAmount, |
| 939 |
'inclusive' => $isInclusive, |
| 940 |
]; |
| 941 |
} |
| 942 |
|
| 943 |
return $normalizedRates; |
| 944 |
} |
| 945 |
|
| 946 |
protected function getCheckoutLineTaxShortLabel($label): string |
| 947 |
{ |
| 948 |
$label = trim((string) $label); |
| 949 |
|
| 950 |
if (!$label) { |
| 951 |
return __('Tax', 'fluent-cart'); |
| 952 |
} |
| 953 |
|
| 954 |
return $label; |
| 955 |
} |
| 956 |
|
| 957 |
private function shouldRateStrikethrough($isReversed, $rateIsInclusive, $rcMode): bool |
| 958 |
{ |
| 959 |
if (!$isReversed) { |
| 960 |
return false; |
| 961 |
} |
| 962 |
return !$rateIsInclusive || $rcMode === 'dynamic'; |
| 963 |
} |
| 964 |
} |
| 965 |
|