PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
← All changes | app/Modules/Tax/TaxModule.php +34 -909 1.5.4 → 1.6.5 View file →
@@ -8,8 +8,9 @@
8 8 use FluentCart\Framework\Support\Arr;
9 9 use FluentCart\App\Helpers\CartHelper;
10 10 use FluentCart\App\Models\Cart;
11 11 use FluentCart\App\Models\OrderTaxRate;
12 +use FluentCart\App\Services\Renderer\TaxRenderer;
12 13 use FluentCart\App\Services\Renderer\VatFieldRenderer;
13 14 use FluentCart\App\Services\Renderer\CartSummaryRender;
14 15 use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper;
15 16 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
@@ -21,8 +22,18 @@
21 22 {
22 23
23 24 protected $taxSettings = [];
24 25
26 + protected $renderer = null;
27 +
28 + private function renderer(): TaxRenderer
29 + {
30 + if (!$this->renderer) {
31 + $this->renderer = new TaxRenderer($this);
32 + }
33 + return $this->renderer;
34 + }
35 +
25 36 public function register()
26 37 {
27 38 $this->getSettings();
28 39
@@ -144,12 +155,21 @@
144 155
145 156 add_action('fluent_cart/checkout/prepare_other_data', [$this, 'prepareOtherData'], 10, 1);
146 157
147 158 add_action('fluent_cart/product/after_price', function ($data) {
148 - if (Arr::get($data, 'scope') === 'price_range') {
159 + $variant = Arr::get($data, 'variant', null);
160 +
161 + // scope 'price_range' covers two cases: a single confirmed price
162 + // for a simple product (variant is set — resolve its suffix
163 + // normally) and the non-simple price summary (min/max, or a
164 + // single value when every variant is priced the same — variant
165 + // is always null there, deliberately skipped since no single
166 + // tax_inclusion applies to a multi-variant summary; see
167 + // PR #1767/#2271).
168 + if (Arr::get($data, 'scope') === 'price_range' && !$variant) {
149 169 return;
150 170 }
151 - $variant = Arr::get($data, 'variant', null);
171 +
152 172 $priceSuffix = $this->resolvePriceSuffix($variant);
153 173 $variantInclusion = $variant ? Arr::get($variant->other_info ?: [], 'tax_inclusion', '') : '';
154 174 $taxInclusion = $variantInclusion ?: Arr::get($this->taxSettings, 'tax_inclusion', '');
155 175
@@ -203,450 +223,19 @@
203 223 }
204 224
205 225 public function renderTaxRow($cart, $atts = '')
206 226 {
207 - if ($this->getCheckoutTaxBreakdownDisplayMode() === 'simplified') {
208 - return;
209 - }
210 -
211 - $taxAmount = (int) Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
212 - $reversedTaxAmount = (int) Arr::get($cart->checkout_data, 'tax_data.reverse_charge_tax_total', 0);
213 - $isReverseCharge = $this->isReverseChargeCheckout($cart->checkout_data);
214 - $taxLines = Arr::get($cart->checkout_data, 'tax_data.tax_lines', []);
215 - $taxCountry = Arr::get($cart->checkout_data, 'tax_data.tax_country', '');
216 - $taxLabel = Arr::get($taxLines, '0.label', '');
217 -
218 - if (!$taxLabel) {
219 - $taxLabel = $taxCountry ? static::getCountryTaxTitle($taxCountry) : __('Tax', 'fluent-cart');
220 - }
221 - ?>
222 - <li <?php echo $atts; ?>>
223 - <span class="fct_summary_label">
224 - <?php echo esc_html($taxLabel); ?>
225 - </span>
226 - <span class="fct_summary_value">
227 - <?php if ($isReverseCharge && $taxAmount === 0) : ?>
228 - <?php
229 - /* translators: %1$s: formatted reversed tax amount */
230 - echo esc_html(sprintf(__('Tax reversed: %1$s', 'fluent-cart'), Helper::toDecimal($reversedTaxAmount)));
231 - ?>
232 - <?php else : ?>
233 - <?php echo esc_html(Helper::toDecimal($taxAmount)); ?>
234 - <?php endif; ?>
235 - </span>
236 - </li>
237 - <?php
227 + $this->renderer()->renderTaxRow($cart, $atts);
238 228 }
239 229
240 230 public function renderShippingTaxRow($cart, $atts = '')
241 231 {
242 - if ($this->getCheckoutTaxBreakdownDisplayMode() === 'simplified') {
243 - return '';
244 - }
245 -
246 - $shippingTax = (int) Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
247 - $isReverseCharge = $this->isReverseChargeCheckout($cart->checkout_data);
248 - $rcShippingTax = (int) Arr::get($cart->checkout_data, 'tax_data.reverse_charge_shipping_tax', 0);
249 -
250 - if ($shippingTax <= 0 && (!$isReverseCharge || $rcShippingTax <= 0)) {
251 - return '';
252 - }
253 -
254 - $displayAmount = $isReverseCharge ? $rcShippingTax : $shippingTax;
255 - $storeBehavior = (int) Arr::get($cart->checkout_data, 'tax_data.store_tax_behavior',
256 - Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2));
257 - $isInclusive = $storeBehavior === 2;
258 - ?>
259 - <li data-fct-shipping-tax-row <?php echo $atts; ?>>
260 - <span class="fct_summary_label">
261 - <?php if ($isInclusive) : ?>
262 - <?php echo esc_html__('Shipping Tax (Included)', 'fluent-cart'); ?>
263 - <?php else : ?>
264 - <?php echo esc_html__('Shipping Tax (Excluded)', 'fluent-cart'); ?>
265 - <?php endif; ?>
266 - </span>
267 - <span class="fct_summary_value"<?php echo $isReverseCharge ? ' style="text-decoration:line-through;opacity:0.6;"' : ''; ?>>
268 - <?php echo esc_html(Helper::toDecimal($displayAmount)); ?>
269 - </span>
270 - </li>
271 - <?php
272 - return '';
232 + return $this->renderer()->renderShippingTaxRow($cart, $atts);
273 233 }
274 234
275 235 public function renderTaxSummaryBox($cart)
276 236 {
277 - $taxData = Arr::get($cart->checkout_data, 'tax_data', []);
278 - $taxTotal = (int) Arr::get($taxData, 'tax_total', 0);
279 - $exclusiveTaxTotal = (int) Arr::get($taxData, 'exclusive_tax_total', $taxTotal);
280 - $feeTaxLines = (array) Arr::get($taxData, 'fee_tax_lines', []);
281 - $shippingTax = (int) Arr::get($taxData, 'shipping_tax', 0);
282 - $isReverseCharge = $this->isReverseChargeCheckout($cart->checkout_data);
283 - $reversedTaxTotalDisplay = (int) Arr::get($taxData, 'reverse_charge_tax_total', 0);
284 -
285 - $taxRateLines = (array) Arr::get($taxData, 'tax_lines', []);
286 - $shippingTaxLines = (array) Arr::get($taxData, 'shipping_tax_lines', []);
287 -
288 - $inclusiveFeeTax = 0;
289 - $exclusiveFeeTax = 0;
290 - foreach ($feeTaxLines as $feeTaxLine) {
291 - if (!empty($feeTaxLine['inclusive'])) {
292 - $inclusiveFeeTax += (int) Arr::get($feeTaxLine, 'tax_amount', 0);
293 - } else {
294 - $exclusiveFeeTax += (int) Arr::get($feeTaxLine, 'tax_amount', 0);
295 - }
296 - }
297 - // Fallback: if no fee_tax_lines (old cart data), use aggregate fee_tax as exclusive
298 - if (empty($feeTaxLines)) {
299 - $exclusiveFeeTax = (int) Arr::get($taxData, 'fee_tax', 0);
300 - }
301 -
302 - $inclusiveTax = max(0, $taxTotal - $exclusiveTaxTotal - $exclusiveFeeTax - $inclusiveFeeTax);
303 - $productExclusiveTax = max(0, $exclusiveTaxTotal);
304 -
305 - $isShippingInclusive = \FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper::isShippingTaxInclusiveFromTaxData(
306 - is_array($taxData) ? $taxData : []
307 - );
308 -
309 - $payableTax = $productExclusiveTax + $exclusiveFeeTax + ($isShippingInclusive ? 0 : $shippingTax);
310 - $totalOrderTax = $inclusiveTax + $inclusiveFeeTax + ($isShippingInclusive ? $shippingTax : 0) + $payableTax;
311 -
312 - $feeRows = \FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper::buildFeeTaxLineRows($feeTaxLines);
313 - $feeCount = count($feeRows);
314 - if (empty($feeTaxLines) && (int) Arr::get($taxData, 'fee_tax', 0) > 0) {
315 - $feeCount = 1;
316 - }
317 - $productTaxRowCount = !empty($taxRateLines)
318 - ? count($taxRateLines)
319 - : (int) ($inclusiveTax > 0) + (int) ($productExclusiveTax > 0);
320 - $rowCount = $productTaxRowCount + $feeCount + (int) ($shippingTax > 0);
321 - $shouldShowBreakdown = !empty($taxRateLines)
322 - || !empty($shippingTaxLines)
323 - || $rowCount >= 2
324 - || ($rowCount === 1 && !($payableTax > 0 || $inclusiveTax > 0 || $inclusiveFeeTax > 0));
325 -
326 - // Build folded per-rate rows via shared helper. Cart tax_lines carry only `label`
327 - // (no percent), so we pre-augment each line with `rate_label` = "Label (X%)" so the
328 - // helper picks it up and produces the same display as the old inline block.
329 - $foldedSource = [];
330 - foreach ($taxRateLines as $rateKey => $rateLine) {
331 - $ratePercent = (float) Arr::get($rateLine, 'rate_percent', 0);
332 - $rateLine['rate_label'] = (string) Arr::get($rateLine, 'label', '');
333 - if ($ratePercent > 0) {
334 - $rateLine['rate_label'] .= ' (' . Helper::formatTaxRatePercent($ratePercent) . '%)';
335 - }
336 - $foldedSource[$rateKey] = $rateLine;
337 - }
338 - // Fixed-mode reverse charge leaves tax-inclusive prices (and their embedded VAT)
339 - // untouched — only dynamic mode reverses the inclusive portion. Exclude inclusive
340 - // lines from the map in fixed mode so the rate rows sum to the reversed total.
341 - $rcNonDynamic = $isReverseCharge
342 - && Arr::get($taxData, 'reverse_charge_price_mode', 'fixed') !== 'dynamic';
343 - $rateBaseMap = TaxSummaryHelper::computeRateBaseMap((array) ($cart->cart_data ?: []), $rcNonDynamic);
344 - if ($isReverseCharge) {
345 - // Under reverse charge the stored tax_lines amounts are zeroed at calc time,
346 - // but the original per-rate amounts survive in item line_meta. Restore them
347 - // (and the pre-zeroing shipping lines) so the box renders the same
348 - // breakdown-by-rate table as a normal order.
349 - foreach ($foldedSource as $rateKey => &$foldedLine) {
350 - $rid = (int) Arr::get($foldedLine, 'rate_id', $rateKey);
351 - if (isset($rateBaseMap[$rid])) {
352 - $foldedLine['tax_amount'] = (int) round($rateBaseMap[$rid]['tax']);
353 - }
354 - }
355 - unset($foldedLine);
356 - if ($rcNonDynamic) {
357 - // Rates present only on inclusive lines have nothing reversible in
358 - // fixed mode — drop them instead of rendering a zero row.
359 - foreach (array_keys($foldedSource) as $rateKey) {
360 - $rid = (int) Arr::get($foldedSource[$rateKey], 'rate_id', $rateKey);
361 - if (!isset($rateBaseMap[$rid])) {
362 - unset($foldedSource[$rateKey]);
363 - }
364 - }
365 - }
366 - $shippingTaxLines = (array) Arr::get($taxData, 'reverse_charge_shipping_tax_lines', []);
367 - }
368 - $checkoutRateRows = TaxSummaryHelper::buildFoldedRateRows(
369 - $foldedSource, $shippingTaxLines, 'tax_amount', $isShippingInclusive, $rateBaseMap
370 - );
371 - // Inclusive shipping tax follows the store global tax mode — when shipping is
372 - // priced inclusive its tax is baked into the shipping price, so it counts as
373 - // "of which included in prices". Keeps includedInPrices + payableTax === totalOrderTax.
374 - $includedInPrices = $inclusiveTax + $inclusiveFeeTax + ($isShippingInclusive ? $shippingTax : 0);
375 -
376 - $displayMode = $this->getCheckoutTaxBreakdownDisplayMode();
377 - $isSimplified = $displayMode === 'simplified';
378 - $simpleLabel = $this->getTaxDisplayLabel();
379 - if ($isReverseCharge) {
380 - $simpleValue = __('Reverse charge', 'fluent-cart');
381 - } elseif ($payableTax === 0 && $totalOrderTax > 0) {
382 - $suffix = (string) Arr::get($this->getSettings(), 'price_suffix_included', '');
383 - if ($suffix === '') {
384 - $suffix = __('(incl.)', 'fluent-cart');
385 - }
386 - /* translators: %1$s: formatted tax amount, %2$s: inclusive suffix */
387 - $simpleValue = sprintf(__('%1$s %2$s', 'fluent-cart'), html_entity_decode(Helper::toDecimal($totalOrderTax), ENT_QUOTES, 'UTF-8'), $suffix);
388 - } else {
389 - $simpleValue = html_entity_decode(Helper::toDecimal($payableTax), ENT_QUOTES, 'UTF-8');
390 - }
391 -
392 - $tooltipId = 'fct-tax-summary-tooltip-' . Helper::getUidSerial();
393 - ?>
394 - <li class="fct_tax_summary_li" data-fct-tax-summary>
395 -<?php if ($isSimplified) : ?>
396 - <style>
397 - .fct_tax_simple_details > summary { list-style: none; cursor: pointer; }
398 - .fct_tax_simple_details > summary::-webkit-details-marker { display: none; }
399 - .fct_tax_simple_toggle { color: #2563eb; text-decoration: none; font-size: 11px; }
400 - .fct_tax_simple_label { color: inherit; }
401 - </style>
402 - <details class="fct_tax_simple_details">
403 - <summary class="fct_tax_simple_summary" style="display:flex;justify-content:space-between;align-items:center;gap:8px;cursor:pointer;list-style:none;">
404 - <span class="fct_tax_simple_left">
405 - <span class="fct_tax_simple_label"><?php echo esc_html($simpleLabel); ?></span>
406 - <span class="fct_tax_simple_toggle"><?php esc_html_e('See details', 'fluent-cart'); ?> &#9662;</span>
407 - </span>
408 - <span class="fct_tax_simple_value"><?php echo esc_html($simpleValue); ?></span>
409 - </summary>
410 -<?php endif; ?>
411 - <div class="fct_tax_summary_box">
412 - <div class="fct_tax_summary_header">
413 - <span class="fct_tax_summary_heading">
414 - <?php echo !empty($checkoutRateRows)
415 - ? esc_html__('Tax breakdown by rate', 'fluent-cart')
416 - : esc_html__('TAX', 'fluent-cart'); ?>
417 - </span>
418 - <div class="fct_item_tax_hint">
419 - <button
420 - type="button"
421 - class="fct_item_tax_hint_button"
422 - aria-label="<?php esc_attr_e('Tax information', 'fluent-cart'); ?>"
423 - aria-describedby="<?php echo esc_attr($tooltipId); ?>"
424 - >
425 - <span aria-hidden="true">i</span>
426 - </button>
427 - <div class="fct_item_tax_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip">
428 - <span class="fct_item_tax_tooltip_heading">
429 - <?php esc_html_e('About your tax', 'fluent-cart'); ?>
430 - </span>
431 - <?php if ($isReverseCharge) : ?>
432 - <span class="fct_item_tax_tooltip_line">
433 - <?php esc_html_e('Tax has been reversed for this order.', 'fluent-cart'); ?>
434 - </span>
435 - <?php else : ?>
436 - <?php if ($payableTax > 0) : ?>
437 - <span class="fct_item_tax_tooltip_line">
438 - <?php esc_html_e('"Total payable tax" is added on top of listed prices.', 'fluent-cart'); ?>
439 - </span>
440 - <?php endif; ?>
441 - <?php if ($inclusiveTax > 0) : ?>
442 - <span class="fct_item_tax_tooltip_line">
443 - <?php esc_html_e('"Included in item prices" is already built into product prices.', 'fluent-cart'); ?>
444 - </span>
445 - <?php endif; ?>
446 - <?php if ($payableTax === 0 && $inclusiveTax === 0) : ?>
447 - <span class="fct_item_tax_tooltip_line">
448 - <?php esc_html_e('No tax applies to this order.', 'fluent-cart'); ?>
449 - </span>
450 - <?php endif; ?>
451 - <?php endif; ?>
452 - </div>
453 - </div>
454 - </div>
455 - <div class="fct_tax_summary_rows">
456 - <?php if (!empty($checkoutRateRows)) : ?>
457 - <div class="fct_tax_summary_row fct_tax_summary_row--head" style="display:flex;justify-content:space-between;gap:8px;">
458 - <span style="flex:1;font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:#94a3b8;">
459 - <?php esc_html_e('Rate', 'fluent-cart'); ?>
460 - </span>
461 - <span style="min-width:88px;text-align:right;font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:#94a3b8;">
462 - <?php esc_html_e('Taxable base', 'fluent-cart'); ?>
463 - </span>
464 - <span style="min-width:64px;text-align:right;font-size:11px;text-transform:uppercase;letter-spacing:0.04em;color:#94a3b8;">
465 - <?php esc_html_e('Tax', 'fluent-cart'); ?>
466 - </span>
467 - </div>
468 - <?php foreach ($checkoutRateRows as $checkoutRateRow) : ?>
469 - <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;">
470 - <span class="fct_tax_summary_row_label" style="flex:1;">
471 - <?php echo esc_html($checkoutRateRow['label']); ?>
472 - </span>
473 - <span style="min-width:88px;text-align:right;color:#94a3b8;">
474 - <?php echo esc_html(Helper::toDecimal((int) $checkoutRateRow['base'])); ?>
475 - </span>
476 - <span class="fct_tax_summary_row_amount" style="min-width:64px;text-align:right;">
477 - <?php echo esc_html(Helper::toDecimal((int) $checkoutRateRow['tax'])); ?>
478 - </span>
479 - </div>
480 - <?php endforeach; ?>
481 - <?php endif; ?>
482 - <?php if ($isReverseCharge) : ?>
483 - <?php if (empty($checkoutRateRows)) : ?>
484 - <?php
485 - $rcShippingDisplay = (int) Arr::get($taxData, 'reverse_charge_shipping_tax', 0);
486 - $rcInclusiveAdj = (int) Arr::get($taxData, 'reverse_charge_inclusive_adjustment', 0);
487 - $rcExclusiveNonShip = max(0, $reversedTaxTotalDisplay - $rcShippingDisplay - $rcInclusiveAdj);
488 - $rcBreakdownCount = (int) ($rcInclusiveAdj > 0) + (int) ($rcExclusiveNonShip > 0) + (int) ($rcShippingDisplay > 0);
489 - ?>
490 - <?php if ($rcBreakdownCount >= 2) : ?>
491 - <?php if ($rcInclusiveAdj > 0) : ?>
492 - <div class="fct_tax_summary_row fct_tax_summary_row--muted">
493 - <span class="fct_tax_summary_row_label">
494 - <?php esc_html_e('Included in item prices', 'fluent-cart'); ?>
495 - </span>
496 - <span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;">
497 - <?php echo esc_html(Helper::toDecimal($rcInclusiveAdj)); ?>
498 - </span>
499 - </div>
500 - <?php endif; ?>
501 - <?php if ($rcExclusiveNonShip > 0) : ?>
502 - <div class="fct_tax_summary_row">
503 - <span class="fct_tax_summary_row_label">
504 - <?php esc_html_e('Added on products', 'fluent-cart'); ?>
505 - </span>
506 - <span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;">
507 - <?php echo esc_html(Helper::toDecimal($rcExclusiveNonShip)); ?>
508 - </span>
509 - </div>
510 - <?php endif; ?>
511 - <?php if ($rcShippingDisplay > 0) : ?>
512 - <div class="fct_tax_summary_row<?php echo $isShippingInclusive ? ' fct_tax_summary_row--muted' : ''; ?>">
513 - <span class="fct_tax_summary_row_label">
514 - <?php echo $isShippingInclusive ? esc_html__('Included in shipping prices', 'fluent-cart') : esc_html__('Added on shipping', 'fluent-cart'); ?>
515 - </span>
516 - <span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;">
517 - <?php echo esc_html(Helper::toDecimal($rcShippingDisplay)); ?>
518 - </span>
519 - </div>
520 - <?php endif; ?>
521 - <?php elseif ($rcShippingDisplay > 0) : ?>
522 - <div class="fct_tax_summary_row fct_tax_summary_row--muted">
523 - <span class="fct_tax_summary_row_label">
524 - <?php echo $isShippingInclusive ? esc_html__('Included in shipping prices', 'fluent-cart') : esc_html__('Added on shipping', 'fluent-cart'); ?>
525 - </span>
526 - <span class="fct_tax_summary_row_amount" style="text-decoration:line-through;opacity:0.6;">
527 - <?php echo esc_html(Helper::toDecimal($rcShippingDisplay)); ?>
528 - </span>
529 - </div>
530 - <?php endif; ?>
531 - <?php endif; ?>
532 - <div class="fct_tax_summary_row fct_tax_summary_row--total">
533 - <span class="fct_tax_summary_row_label">
534 - <?php esc_html_e('VAT reversed', 'fluent-cart'); ?>
535 - </span>
536 - <span class="fct_tax_summary_row_amount">
537 - <?php echo esc_html(Helper::toDecimal($reversedTaxTotalDisplay)); ?>
538 - </span>
539 - </div>
540 - <?php else : ?>
541 - <?php if (empty($checkoutRateRows) && $inclusiveTax > 0 && $shouldShowBreakdown) : ?>
542 - <div class="fct_tax_summary_row fct_tax_summary_row--muted">
543 - <span class="fct_tax_summary_row_label">
544 - <?php esc_html_e('Included in item prices', 'fluent-cart'); ?>
545 - </span>
546 - <span class="fct_tax_summary_row_amount">
547 - <?php echo esc_html(Helper::toDecimal($inclusiveTax)); ?>
548 - </span>
549 - </div>
550 - <?php endif; ?>
551 - <?php if (empty($checkoutRateRows) && $productExclusiveTax > 0 && $shouldShowBreakdown) : ?>
552 - <div class="fct_tax_summary_row">
553 - <span class="fct_tax_summary_row_label">
554 - <?php esc_html_e('Added on products', 'fluent-cart'); ?>
555 - </span>
556 - <span class="fct_tax_summary_row_amount">
557 - <?php echo esc_html(Helper::toDecimal($productExclusiveTax)); ?>
558 - </span>
559 - </div>
560 - <?php endif; ?>
561 - <?php if (empty($checkoutRateRows) && $shouldShowBreakdown) : ?>
562 - <?php foreach ($feeRows as $feeRow) : ?>
563 - <div class="fct_tax_summary_row<?php echo $feeRow['inclusive'] ? ' fct_tax_summary_row--muted' : ''; ?>">
564 - <span class="fct_tax_summary_row_label">
565 - <?php echo esc_html($feeRow['display_label']); ?>
566 - </span>
567 - <span class="fct_tax_summary_row_amount">
568 - <?php echo esc_html(Helper::toDecimal($feeRow['tax_amount'])); ?>
569 - </span>
570 - </div>
571 - <?php endforeach; ?>
572 - <?php endif; ?>
573 - <?php if (empty($checkoutRateRows) && empty($feeTaxLines) && (int) Arr::get($taxData, 'fee_tax', 0) > 0 && $shouldShowBreakdown) : ?>
574 - <div class="fct_tax_summary_row">
575 - <span class="fct_tax_summary_row_label">
576 - <?php esc_html_e('Added on fees', 'fluent-cart'); ?>
577 - </span>
578 - <span class="fct_tax_summary_row_amount">
579 - <?php echo esc_html(Helper::toDecimal((int) Arr::get($taxData, 'fee_tax', 0))); ?>
580 - </span>
581 - </div>
582 - <?php endif; ?>
583 - <?php if (empty($checkoutRateRows) && $shippingTax > 0 && $shouldShowBreakdown) : ?>
584 - <?php if (!empty($shippingTaxLines)) : ?>
585 - <?php foreach ($shippingTaxLines as $shippingTaxLine) : ?>
586 - <div class="fct_tax_summary_row<?php echo $isShippingInclusive ? ' fct_tax_summary_row--muted' : ''; ?>">
587 - <span class="fct_tax_summary_row_label">
588 - <?php echo esc_html($shippingTaxLine['label']); ?>
589 - </span>
590 - <span class="fct_tax_summary_row_amount">
591 - <?php echo esc_html(Helper::toDecimal((int) Arr::get($shippingTaxLine, 'shipping_tax', 0))); ?>
592 - </span>
593 - </div>
594 - <?php endforeach; ?>
595 - <?php else : ?>
596 - <div class="fct_tax_summary_row<?php echo $isShippingInclusive ? ' fct_tax_summary_row--muted' : ''; ?>">
597 - <span class="fct_tax_summary_row_label">
598 - <?php if ($isShippingInclusive) : ?>
599 - <?php esc_html_e('Included in shipping prices', 'fluent-cart'); ?>
600 - <?php else : ?>
601 - <?php esc_html_e('Added on shipping', 'fluent-cart'); ?>
602 - <?php endif; ?>
603 - </span>
604 - <span class="fct_tax_summary_row_amount">
605 - <?php echo esc_html(Helper::toDecimal($shippingTax)); ?>
606 - </span>
607 - </div>
608 - <?php endif; ?>
609 - <?php endif; ?>
610 - <?php if (!empty($checkoutRateRows)) : ?>
611 - <div class="fct_tax_summary_row fct_tax_summary_row--total">
612 - <span class="fct_tax_summary_row_label"><?php esc_html_e('Total tax', 'fluent-cart'); ?></span>
613 - <span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($totalOrderTax)); ?></span>
614 - </div>
615 - <?php if ($includedInPrices > 0) : ?>
616 - <div class="fct_tax_summary_row fct_tax_summary_row--muted">
617 - <span class="fct_tax_summary_row_label"><?php esc_html_e('of which included in prices', 'fluent-cart'); ?></span>
618 - <span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($includedInPrices)); ?></span>
619 - </div>
620 - <?php endif; ?>
621 - <?php if ($payableTax > 0 && $includedInPrices > 0) : ?>
622 - <div class="fct_tax_summary_row fct_tax_summary_row--total">
623 - <span class="fct_tax_summary_row_label"><?php esc_html_e('Payable now (added)', 'fluent-cart'); ?></span>
624 - <span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($payableTax)); ?></span>
625 - </div>
626 - <?php endif; ?>
627 - <?php else : ?>
628 - <?php if ($payableTax > 0) : ?>
629 - <div class="fct_tax_summary_row fct_tax_summary_row--total">
630 - <span class="fct_tax_summary_row_label"><?php esc_html_e('Total payable tax', 'fluent-cart'); ?></span>
631 - <span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($payableTax)); ?></span>
632 - </div>
633 - <?php endif; ?>
634 - <?php if ($inclusiveTax > 0 || $inclusiveFeeTax > 0) : ?>
635 - <div class="fct_tax_summary_row fct_tax_summary_row--muted">
636 - <span class="fct_tax_summary_row_label"><?php esc_html_e('Total tax in this order', 'fluent-cart'); ?></span>
637 - <span class="fct_tax_summary_row_amount"><?php echo esc_html(Helper::toDecimal($totalOrderTax)); ?></span>
638 - </div>
639 - <?php endif; ?>
640 - <?php endif; ?>
641 - <?php endif; ?>
642 - </div>
643 - </div>
644 -<?php if ($isSimplified) : ?>
645 - </details>
646 -<?php endif; ?>
647 - </li>
648 - <?php
237 + $this->renderer()->renderTaxSummaryBox($cart);
649 238 }
650 239
651 240 public function maybeRestoreRcAdjustedPrices($data)
652 241 {
@@ -819,511 +408,47 @@
819 408
820 409 return $this->taxSettings = $settings;
821 410 }
822 411
823 - private function getEffectiveRcMode()
412 + public function getEffectiveRcMode()
824 413 {
825 414 $settings = $this->getSettings();
826 415 return Arr::get($settings, 'eu_vat_settings.reverse_charge_price_mode', 'fixed');
827 416 }
828 417
829 - protected function getCheckoutTaxBreakdownDisplayMode()
830 - {
831 - // Backward compat: legacy stored values ('both', 'label', 'tooltip', or anything
832 - // else) all collapse to 'itemized'. Only an explicit 'simplified' stays simplified.
833 - $mode = Arr::get($this->getSettings(), 'checkout_tax_breakdown_display', 'itemized');
834 -
835 - return $mode === 'simplified' ? 'simplified' : 'itemized';
836 - }
837 -
838 - protected function getTaxDisplayLabel()
839 - {
840 - $label = trim((string) Arr::get($this->getSettings(), 'tax_display_label', ''));
841 - return $label !== '' ? $label : __('Tax', 'fluent-cart');
842 - }
843 -
844 - protected function normalizeCheckoutLineTaxRates($item)
845 - {
846 - $rates = Arr::get($item, 'line_meta.tax_config.rates', []);
847 - if (empty($rates) || !is_array($rates)) {
848 - return [];
849 - }
850 -
851 - $isInclusive = (bool) Arr::get($item, 'line_meta.tax_config.inclusive', false);
852 - $normalizedRates = [];
853 -
854 - foreach ($rates as $rate) {
855 - $taxAmount = (int) Arr::get($rate, 'tax_amount', 0);
856 - $ratePercent = (float) Arr::get($rate, 'rate_percent', 0);
857 - $taxableAmount = (int) Arr::get($rate, 'taxable_amount', 0);
858 -
859 - if ($taxAmount <= 0 && $ratePercent <= 0) {
860 - continue;
861 - }
862 -
863 - $normalizedRates[] = [
864 - 'label' => (string) Arr::get($rate, 'label', __('Tax', 'fluent-cart')),
865 - 'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')),
866 - 'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent),
867 - 'tax_amount' => $taxAmount,
868 - 'display_base' => $isInclusive ? max(0, $taxableAmount - $taxAmount) : $taxableAmount,
869 - 'inclusive' => $isInclusive,
870 - ];
871 - }
872 -
873 - return $normalizedRates;
874 - }
875 -
876 - protected function getCheckoutLineTaxShortLabel($label)
877 - {
878 - $label = trim((string) $label);
879 -
880 - if (!$label) {
881 - return __('Tax', 'fluent-cart');
882 - }
883 -
884 - return $label;
885 - }
886 -
887 - private function shouldRateStrikethrough($isReversed, $rateIsInclusive, $rcMode)
888 - {
889 - if (!$isReversed) {
890 - return false;
891 - }
892 - return !$rateIsInclusive || $rcMode === 'dynamic';
893 - }
894 -
895 418 public function renderCheckoutLineItemTaxLabel($data)
896 419 {
897 - $mode = $this->getCheckoutTaxBreakdownDisplayMode();
898 - if ($mode === 'simplified') {
899 - return;
900 - }
901 -
902 - $item = Arr::get($data, 'item', []);
903 - $rates = $this->normalizeCheckoutLineTaxRates($item);
904 - if (empty($rates)) {
905 - return;
906 - }
907 - $cart = CartHelper::getCart();
908 - $isReversed = $cart ? $this->isReverseChargeCheckout($cart->checkout_data) : false;
909 - $rcMode = $this->getEffectiveRcMode();
910 - $this->renderTaxBadges($rates, $isReversed, $rcMode);
420 + $this->renderer()->renderCheckoutLineItemTaxLabel($data);
911 421 }
912 422
913 423 public function renderCheckoutSetupFeeTaxLabel($data)
914 424 {
915 - $mode = $this->getCheckoutTaxBreakdownDisplayMode();
916 - if ($mode === 'simplified') {
917 - return;
918 - }
919 -
920 - $item = Arr::get($data, 'item', []);
921 - $signupFeeTaxConfig = Arr::get($item, 'signup_fee_tax_config', []);
922 - $rates = Arr::get($signupFeeTaxConfig, 'rates', []);
923 - if (empty($rates) || !is_array($rates)) {
924 - return;
925 - }
926 -
927 - $isInclusive = (bool) Arr::get($signupFeeTaxConfig, 'inclusive', false);
928 - $normalizedRates = [];
929 - foreach ($rates as $rate) {
930 - $taxAmount = (int) Arr::get($rate, 'tax_amount', 0);
931 - $ratePercent = (float) Arr::get($rate, 'rate_percent', 0);
932 - if ($taxAmount <= 0 && $ratePercent <= 0) {
933 - continue;
934 - }
935 - $normalizedRates[] = [
936 - 'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')),
937 - 'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent),
938 - 'inclusive' => $isInclusive,
939 - 'tax_amount' => $taxAmount,
940 - ];
941 - }
942 -
943 - if (empty($normalizedRates)) {
944 - return;
945 - }
946 - $cart = CartHelper::getCart();
947 - $isReversed = $cart ? $this->isReverseChargeCheckout($cart->checkout_data) : false;
948 - $rcMode = $this->getEffectiveRcMode();
949 - $this->renderTaxBadges($normalizedRates, $isReversed, $rcMode);
425 + $this->renderer()->renderCheckoutSetupFeeTaxLabel($data);
950 426 }
951 427
952 - private function renderTaxBadges(array $normalizedRates, $isReversed = false, $rcMode = 'fixed')
953 - {
954 - // Fixed-mode reverse charge: inclusive-priced lines keep their gross price and
955 - // no VAT is charged, so an "incl. X" badge would be misleading — hide those rates.
956 - if ($isReversed && $rcMode !== 'dynamic') {
957 - $normalizedRates = array_values(array_filter($normalizedRates, function ($rate) {
958 - return empty($rate['inclusive']);
959 - }));
960 - }
961 - if (empty($normalizedRates)) {
962 - return;
963 - }
964 - ?>
965 - <div class="fct_item_tax_badges" aria-label="<?php esc_attr_e('Tax breakdown', 'fluent-cart'); ?>">
966 - <?php foreach ($normalizedRates as $rate) : ?>
967 - <div class="fct_item_tax_badge_row">
968 - <span class="fct_item_tax_badge <?php echo esc_attr($rate['inclusive'] ? 'is-inclusive' : 'is-exclusive'); ?>">
969 - <span>
970 - <?php
971 - $badgeText = sprintf(
972 - /* translators: %1$s: tax label, %2$s: tax rate percent */
973 - __('%1$s (%2$s%%)', 'fluent-cart'),
974 - $rate['short_label'],
975 - $rate['formatted_rate']
976 - );
977 - echo esc_html($badgeText);
978 - ?>
979 - </span>
980 - </span>
981 - <?php if (!empty($rate['tax_amount']) && (int) $rate['tax_amount'] > 0) : ?>
982 - <?php $rateReversedClass = $this->shouldRateStrikethrough($isReversed, $rate['inclusive'], $rcMode) ? ' is-reversed' : ''; ?>
983 - <span class="fct_item_tax_badge_amount <?php echo esc_attr($rate['inclusive'] ? 'is-inclusive' : 'is-exclusive'); ?><?php echo esc_attr($rateReversedClass); ?>">
984 - <?php
985 - $amountText = $rate['inclusive']
986 - ? sprintf(
987 - /* translators: %1$s: formatted tax amount */
988 - __('incl. %1$s', 'fluent-cart'),
989 - Helper::toDecimal((int) $rate['tax_amount'])
990 - )
991 - : sprintf(
992 - /* translators: %1$s: formatted tax amount */
993 - __('+ %1$s', 'fluent-cart'),
994 - Helper::toDecimal((int) $rate['tax_amount'])
995 - );
996 - echo esc_html($amountText);
997 - ?>
998 - </span>
999 - <?php endif; ?>
1000 - </div>
1001 - <?php endforeach; ?>
1002 - </div>
1003 - <?php
1004 - }
1005 -
1006 - private function getSetupFeeTaxData($item)
1007 - {
1008 - $signupFeeTaxConfig = Arr::get($item, 'signup_fee_tax_config', []);
1009 - $rawRates = Arr::get($signupFeeTaxConfig, 'rates', []);
1010 - if (empty($rawRates) || !is_array($rawRates)) {
1011 - return null;
1012 - }
1013 -
1014 - $isInclusive = (bool) Arr::get($signupFeeTaxConfig, 'inclusive', false);
1015 - $rates = [];
1016 - foreach ($rawRates as $rate) {
1017 - $taxAmount = (int) Arr::get($rate, 'tax_amount', 0);
1018 - $ratePercent = (float) Arr::get($rate, 'rate_percent', 0);
1019 - if ($taxAmount <= 0 && $ratePercent <= 0) {
1020 - continue;
1021 - }
1022 - $taxableAmount = (int) Arr::get($rate, 'taxable_amount', 0);
1023 - $rates[] = [
1024 - 'short_label' => $this->getCheckoutLineTaxShortLabel((string) Arr::get($rate, 'label', '')),
1025 - 'formatted_rate' => Helper::formatTaxRatePercent((float) $ratePercent),
1026 - 'tax_amount' => $taxAmount,
1027 - 'display_base' => $isInclusive ? max(0, $taxableAmount - $taxAmount) : $taxableAmount,
1028 - ];
1029 - }
1030 -
1031 - if (empty($rates)) {
1032 - return null;
1033 - }
1034 -
1035 - $setupFee = (int) Arr::get($item, 'other_info.signup_fee', 0);
1036 - $totalTax = array_sum(array_map(function ($rate) {
1037 - return (int) $rate['tax_amount'];
1038 - }, $rates));
1039 -
1040 - return [
1041 - 'rates' => $rates,
1042 - 'is_inclusive' => $isInclusive,
1043 - 'total_tax' => $totalTax,
1044 - 'display_total' => $isInclusive ? $setupFee : $setupFee + $totalTax,
1045 - 'primary_label' => Arr::get($rates, '0.short_label', __('Tax', 'fluent-cart')),
1046 - ];
1047 - }
1048 -
1049 428 public function renderCheckoutSetupFeeTaxTooltip($data)
1050 429 {
1051 - $mode = $this->getCheckoutTaxBreakdownDisplayMode();
1052 - if ($mode === 'simplified') {
1053 - return;
1054 - }
1055 -
1056 - $item = Arr::get($data, 'item', []);
1057 - $taxData = $this->getSetupFeeTaxData($item);
1058 - if (!$taxData) {
1059 - return;
1060 - }
1061 -
1062 - $cart = CartHelper::getCart();
1063 - $isReversed = $cart ? $this->isReverseChargeCheckout($cart->checkout_data) : false;
1064 - $rcMode = $this->getEffectiveRcMode();
1065 -
1066 - $rates = $taxData['rates'];
1067 - $isInclusive = $taxData['is_inclusive'];
1068 - $displayTotal = $taxData['display_total'];
1069 - if ($isReversed) {
1070 - if ($taxData['is_inclusive'] && $rcMode === 'dynamic') {
1071 - $displayTotal = $taxData['display_total'] - $taxData['total_tax'];
1072 - } else {
1073 - $displayTotal = (int) Arr::get($data, 'item.other_info.signup_fee', 0);
1074 - }
1075 - }
1076 - $tooltipId = 'fct-item-tax-tooltip-' . Helper::getUidSerial();
1077 - ?>
1078 - <div class="fct_item_tax_hint">
1079 - <button
1080 - type="button"
1081 - class="fct_item_tax_hint_button"
1082 - aria-label="<?php esc_attr_e('View tax breakdown for this item', 'fluent-cart'); ?>"
1083 - aria-describedby="<?php echo esc_attr($tooltipId); ?>"
1084 - >
1085 - <span aria-hidden="true">i</span>
1086 - </button>
1087 - <div class="fct_item_tax_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip">
1088 - <span class="fct_item_tax_tooltip_heading">
1089 - <?php echo esc_html($isInclusive ? __('Tax-inclusive price', 'fluent-cart') : __('Tax-exclusive price', 'fluent-cart')); ?>
1090 - </span>
1091 - <?php foreach ($rates as $rate) : ?>
1092 - <?php $lineReversedClass = $this->shouldRateStrikethrough($isReversed, isset($rate['inclusive']) ? (bool)$rate['inclusive'] : $taxData['is_inclusive'], $rcMode) ? ' is-reversed' : ''; ?>
1093 - <span class="fct_item_tax_tooltip_line<?php echo esc_attr($lineReversedClass); ?>">
1094 - <?php
1095 - $lineText = sprintf(
1096 - /* translators: %1$s: tax base amount, %2$s: tax label, %3$s: tax rate percent, %4$s: tax amount */
1097 - __('Base %1$s + %2$s %3$s%% %4$s', 'fluent-cart'),
1098 - Helper::toDecimal($rate['display_base']),
1099 - $rate['short_label'],
1100 - $rate['formatted_rate'],
1101 - Helper::toDecimal($rate['tax_amount'])
1102 - );
1103 - echo esc_html($lineText);
1104 - ?>
1105 - </span>
1106 - <?php endforeach; ?>
1107 - <span class="fct_item_tax_tooltip_line is-total">
1108 - <?php
1109 - echo esc_html(sprintf(
1110 - /* translators: %1$s: line total amount */
1111 - __('Total %1$s', 'fluent-cart'),
1112 - Helper::toDecimal($displayTotal)
1113 - ));
1114 - ?>
1115 - </span>
1116 - </div>
1117 - </div>
1118 - <?php
430 + $this->renderer()->renderCheckoutSetupFeeTaxTooltip($data);
1119 431 }
1120 432
1121 433 public function renderCheckoutSetupFeeTaxInfo($data)
1122 434 {
1123 - $mode = $this->getCheckoutTaxBreakdownDisplayMode();
1124 - if ($mode === 'simplified') {
1125 - return;
1126 - }
1127 -
1128 - $item = Arr::get($data, 'item', []);
1129 - $taxData = $this->getSetupFeeTaxData($item);
1130 - if (!$taxData) {
1131 - return;
1132 - }
1133 -
1134 - $isInclusive = $taxData['is_inclusive'];
1135 - $totalTax = $taxData['total_tax'];
1136 - $primaryLabel = $taxData['primary_label'];
1137 -
1138 - $priceNote = $isInclusive
1139 - ? sprintf(
1140 - /* translators: %1$s: tax label */
1141 - __('%1$s incl.', 'fluent-cart'),
1142 - $primaryLabel
1143 - )
1144 - : sprintf(
1145 - /* translators: %1$s: tax amount, %2$s: tax label */
1146 - __('+ %1$s %2$s', 'fluent-cart'),
1147 - Helper::toDecimal($totalTax),
1148 - strtolower($primaryLabel)
1149 - );
1150 - ?>
1151 - <span class="fct_setup_fee_price_note"><?php echo esc_html($priceNote); ?></span>
1152 - <?php
435 + $this->renderer()->renderCheckoutSetupFeeTaxInfo($data);
1153 436 }
1154 437
1155 438 public function renderCheckoutLineItemTaxTooltip($data)
1156 439 {
1157 - $mode = $this->getCheckoutTaxBreakdownDisplayMode();
1158 - if ($mode === 'simplified') {
1159 - return;
1160 - }
1161 -
1162 - $item = Arr::get($data, 'item', []);
1163 - $rates = $this->normalizeCheckoutLineTaxRates($item);
1164 - if (empty($rates)) {
1165 - return;
1166 - }
1167 -
1168 - $cart = CartHelper::getCart();
1169 - $isReversed = $cart ? $this->isReverseChargeCheckout($cart->checkout_data) : false;
1170 - $rcMode = $this->getEffectiveRcMode();
1171 -
1172 - $isInclusive = (bool) Arr::get($rates, '0.inclusive', false);
1173 - $itemSubtotal = (int) Arr::get($item, 'line_total', Arr::get($item, 'subtotal', 0));
1174 - $itemTaxAmount = array_sum(array_map(function ($rate) {
1175 - return (int) Arr::get($rate, 'tax_amount', 0);
1176 - }, $rates));
1177 - if ($isReversed) {
1178 - if ($isInclusive && $rcMode === 'dynamic') {
1179 - $displayTotal = $itemSubtotal - $itemTaxAmount;
1180 - } else {
1181 - $displayTotal = $itemSubtotal;
1182 - }
1183 - } else {
1184 - $displayTotal = $isInclusive ? $itemSubtotal : $itemSubtotal + $itemTaxAmount;
1185 - }
1186 - $tooltipId = 'fct-item-tax-tooltip-' . Helper::getUidSerial();
1187 - ?>
1188 - <div class="fct_item_tax_hint">
1189 - <button
1190 - type="button"
1191 - class="fct_item_tax_hint_button"
1192 - aria-label="<?php esc_attr_e('View tax breakdown for this item', 'fluent-cart'); ?>"
1193 - aria-describedby="<?php echo esc_attr($tooltipId); ?>"
1194 - >
1195 - <span aria-hidden="true">i</span>
1196 - </button>
1197 - <div class="fct_item_tax_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip">
1198 - <span class="fct_item_tax_tooltip_heading">
1199 - <?php echo esc_html($isInclusive ? __('Tax-inclusive price', 'fluent-cart') : __('Tax-exclusive price', 'fluent-cart')); ?>
1200 - </span>
1201 - <?php foreach ($rates as $rate) : ?>
1202 - <?php $lineReversedClass = $this->shouldRateStrikethrough($isReversed, $rate['inclusive'], $rcMode) ? ' is-reversed' : ''; ?>
1203 - <span class="fct_item_tax_tooltip_line<?php echo esc_attr($lineReversedClass); ?>">
1204 - <?php
1205 - $lineText = sprintf(
1206 - /* translators: %1$s: tax base amount, %2$s: tax label, %3$s: tax rate percent, %4$s: tax amount */
1207 - __('Base %1$s + %2$s %3$s%% %4$s', 'fluent-cart'),
1208 - Helper::toDecimal($rate['display_base']),
1209 - $rate['short_label'],
1210 - $rate['formatted_rate'],
1211 - Helper::toDecimal($rate['tax_amount'])
1212 - );
1213 - echo esc_html($lineText);
1214 - ?>
1215 - </span>
1216 - <?php endforeach; ?>
1217 - <span class="fct_item_tax_tooltip_line is-total">
1218 - <?php
1219 - echo esc_html(sprintf(
1220 - /* translators: %1$s: line total amount */
1221 - __('Total %1$s', 'fluent-cart'),
1222 - Helper::toDecimal($displayTotal)
1223 - ));
1224 - ?>
1225 - </span>
1226 - </div>
1227 - </div>
1228 - <?php
440 + $this->renderer()->renderCheckoutLineItemTaxTooltip($data);
1229 441 }
1230 442
1231 443 public function renderCheckoutLineItemTaxInfo($data)
1232 444 {
1233 - $mode = $this->getCheckoutTaxBreakdownDisplayMode();
1234 - if ($mode === 'simplified') {
1235 - return;
1236 - }
1237 -
1238 - $item = Arr::get($data, 'item', []);
1239 - $rates = $this->normalizeCheckoutLineTaxRates($item);
1240 - if (empty($rates)) {
1241 - return;
1242 - }
1243 -
1244 - $isInclusive = (bool) Arr::get($rates, '0.inclusive', false);
1245 - $itemTaxAmount = array_sum(array_map(function ($rate) {
1246 - return (int) Arr::get($rate, 'tax_amount', 0);
1247 - }, $rates));
1248 - $primaryLabel = Arr::get($rates, '0.short_label', __('Tax', 'fluent-cart'));
1249 -
1250 - $priceNote = $isInclusive
1251 - ? sprintf(
1252 - /* translators: %1$s: tax label */
1253 - __('%1$s incl.', 'fluent-cart'),
1254 - $primaryLabel
1255 - )
1256 - : sprintf(
1257 - /* translators: %1$s: tax amount, %2$s: tax label */
1258 - __('+ %1$s %2$s', 'fluent-cart'),
1259 - Helper::toDecimal($itemTaxAmount),
1260 - strtolower($primaryLabel)
1261 - );
1262 - ?>
1263 - <div class="fct_item_tax_price_note"><?php echo esc_html($priceNote); ?></div>
1264 - <?php
445 + $this->renderer()->renderCheckoutLineItemTaxInfo($data);
1265 446 }
1266 447
1267 448 public function renderUnitPriceRoundingTooltip($data)
1268 449 {
1269 - $item = Arr::get($data, 'item', []);
1270 - $quantity = (int) Arr::get($item, 'quantity', 1);
1271 -
1272 - if ($quantity < 2) {
1273 - return;
1274 - }
1275 -
1276 - $unitPrice = (int) Arr::get($item, 'unit_price', 0);
1277 - $subtotal = (int) Arr::get($item, 'subtotal', 0);
1278 -
1279 - if ($unitPrice <= 0) {
1280 - return;
1281 - }
1282 -
1283 - $shouldShow = false;
1284 -
1285 - // Case 1 — TaxModule RC dynamic: the applied per-unit tax adjustment is rounded,
1286 - // so unit_price * qty may differ from the exact net by up to (qty - 1) cents.
1287 - $rcAdjustment = (int) Arr::get($item, 'line_meta.reverse_charge_adjustment', 0);
1288 - if ($rcAdjustment > 0) {
1289 - $rates = (array) Arr::get($item, 'line_meta.tax_config.rates', []);
1290 - $actualTaxTotal = (int) array_sum(array_map('intval', array_column($rates, 'tax_amount')));
1291 - $diff = abs($rcAdjustment - $actualTaxTotal);
1292 - if ($diff > 0 && $diff <= $quantity) {
1293 - $shouldShow = true;
1294 - }
1295 - }
1296 -
1297 - // Case 2 — generic: unit_price * qty already doesn't match subtotal
1298 - // (future dynamic pricing or any other module that stores a rounded unit_price)
1299 - if (!$shouldShow && $subtotal > 0) {
1300 - $diff = abs(($unitPrice * $quantity) - $subtotal);
1301 - if ($diff > 0 && $diff <= $quantity) {
1302 - $shouldShow = true;
1303 - }
1304 - }
1305 -
1306 - if (!$shouldShow) {
1307 - return;
1308 - }
1309 -
1310 - $tooltipId = 'fct-unit-price-rounding-' . Helper::getUidSerial();
1311 - ?>
1312 - <div class="fct_item_tax_hint">
1313 - <button
1314 - type="button"
1315 - class="fct_item_tax_hint_button"
1316 - aria-label="<?php esc_attr_e('Unit price rounding information', 'fluent-cart'); ?>"
1317 - aria-describedby="<?php echo esc_attr($tooltipId); ?>"
1318 - >
1319 - <span aria-hidden="true">i</span>
1320 - </button>
1321 - <div class="fct_item_tax_tooltip fct_unit_price_rounding_tooltip" id="<?php echo esc_attr($tooltipId); ?>" role="tooltip">
1322 - <?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'); ?>
1323 - </div>
1324 - </div>
1325 - <?php
450 + $this->renderer()->renderUnitPriceRoundingTooltip($data);
1326 451 }
1327 452
1328 453 public function isEnabled()
1329 454 {
@@ -1715,9 +840,9 @@
1715 840
1716 841 return true;
1717 842 }
1718 843
1719 - protected function isReverseChargeCheckout($checkoutData)
844 + public function isReverseChargeCheckout($checkoutData)
1720 845 {
1721 846 return Arr::get($checkoutData, 'tax_data.valid', false)
1722 847 && (int) Arr::get($checkoutData, 'tax_data.tax_behavior', 2) === 0;
1723 848 }