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