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/Services/ShortCodeParser/Parsers/OrderParser.php +375 -57 1.3.28 → 1.6.5 View file →
@@ -6,14 +6,16 @@
6 6 use FluentCart\Api\StoreSettings;
7 7 use FluentCart\App\Helpers\Helper;
8 8 use FluentCart\App\Helpers\Status;
9 9 use FluentCart\App\Models\Order;
10 -use FluentCart\App\Models\OrderMeta;
11 10 use FluentCart\App\Models\Subscription;
12 11 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
13 12 use FluentCart\App\Services\DateTime\DateTime;
13 +use FluentCart\App\Services\DateTime\DateFormatter;
14 +use FluentCart\App\Services\Payments\PaymentHelper;
14 15 use FluentCart\App\Services\Payments\PaymentReceipt;
15 16 use FluentCart\App\Services\TemplateService;
17 +use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper;
16 18 use FluentCart\Framework\Support\Arr;
17 19 use FluentCart\Framework\Support\Str;
18 20 use FluentCartPro\App\Modules\Licensing\Models\License;
19 21
@@ -54,11 +56,23 @@
54 56
55 57 protected array $methodMap = [
56 58 'item_count' => 'getItemCount',
57 59 'is_digital' => 'getIsDigital',
58 - 'store_vat_display' => 'getStoreVatDisplay',
59 - 'buyer_vat_display' => 'getBuyerVatDisplay',
60 - 'buyer_company_name' => 'getBuyerCompanyName',
60 + 'store_vat_display' => 'getStoreVatDisplay',
61 + 'store_company_name' => 'getStoreCompanyName',
62 + 'store_company_display' => 'getStoreCompanyDisplay',
63 + 'store_legal_registration_id' => 'getStoreLegalRegistrationId',
64 + 'store_legal_registration_display' => 'getStoreLegalRegistrationDisplay',
65 + 'store_seller_vat_id' => 'getStoreSellerVatId',
66 + 'store_seller_vat_display' => 'getStoreSellerVatDisplay',
67 + 'store_seller_tax_id' => 'getStoreSellerTaxId',
68 + 'store_tax_display' => 'getStoreTaxDisplay',
69 + 'buyer_vat_display' => 'getBuyerVatDisplay',
70 + 'buyer_company_name' => 'getBuyerCompanyName',
71 + 'buyer_legal_registration_id' => 'getBuyerLegalRegistrationId',
72 + 'buyer_reverse_charge_declaration' => 'getBuyerReverseChargeDeclaration',
73 + 'tax_breakdown' => 'getTaxBreakdown',
74 + 'fee_lines' => 'getFeeLines',
61 75 ];
62 76
63 77 protected array $attributeMap = [
64 78 'id' => 'order.id',
@@ -91,14 +105,12 @@
91 105 if (in_array($accessor, ['updated_at', 'created_at'])) {
92 106 $date = Arr::get($this->data, $this->attributeMap[$accessor]);
93 107 $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp();
94 108
95 - $date = wp_date(
96 - get_option('date_format'),
97 - $timestamp,
98 - new \DateTimeZone($this->orderTz)
99 - );
100 -
109 + // Through DateFormatter so the store's format and timezone settings
110 + // reach this too; orderTz is only consulted on the Browser source.
111 + $date = DateFormatter::format($timestamp, false, $this->orderTz);
112 +
101 113 return Helper::translateNumber($date);
102 114 }
103 115
104 116 // Handle _formatted suffix for cent columns (e.g. total_amount_formatted)
@@ -348,8 +360,17 @@
348 360
349 361 return is_user_logged_in() ? $orderLink : wp_login_url($orderLink);
350 362 }
351 363
364 + public function getPaymentLink($accessor = null, $code = null)
365 + {
366 + if (empty($this->order)) {
367 + return $code;
368 + }
369 +
370 + return PaymentHelper::getCustomPaymentLink(Arr::get($this->order, 'uuid'));
371 + }
372 +
352 373 public function getAdminOrderLink($accessor, $code = null)
353 374 {
354 375 if (empty($this->order)) {
355 376 return $code;
@@ -479,8 +500,35 @@
479 500
480 501 return ucwords(str_replace(['_', '-'], ' ', $slug));
481 502 }
482 503
504 + public function getFeeLines(): string
505 + {
506 + if (!$this->order) {
507 + return '';
508 + }
509 +
510 + $currency = (string) Arr::get($this->order, 'currency', '');
511 + $feeItems = $this->order->feeItems()->get();
512 +
513 + if ($feeItems->isEmpty()) {
514 + return '';
515 + }
516 +
517 + $rowStyle = 'padding:3px 0;font-size:11px;font-weight:600;color:#525866;';
518 + $valueStyle = 'text-align:right;';
519 + $rows = '';
520 +
521 + foreach ($feeItems as $feeItem) {
522 + $rows .= '<tr>'
523 + . '<td style="' . $rowStyle . '">' . esc_html((string) $feeItem->title) . '</td>'
524 + . '<td style="' . $rowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml((int) $feeItem->subtotal, $currency) . '</td>'
525 + . '</tr>';
526 + }
527 +
528 + return $rows;
529 + }
530 +
483 531 public function getTaxBreakdown(): string
484 532 {
485 533 if (!$this->order) {
486 534 return '';
@@ -485,52 +533,238 @@
485 533 if (!$this->order) {
486 534 return '';
487 535 }
488 536
489 - $currency = (string) Arr::get($this->order, 'currency', '');
490 - $taxTotal = (int) Arr::get($this->order, 'tax_total', 0);
491 - $shippingTax = (int) Arr::get($this->order, 'shipping_tax', 0);
492 - $rowStyle = 'padding:5px 0;font-size:11px;';
493 - $labelStyle = $rowStyle . 'color:#525866;';
494 - $valueStyle = $rowStyle . 'color:#0E121B;text-align:right;';
537 + $currency = (string) Arr::get($this->order, 'currency', '');
538 + $summary = TaxSummaryHelper::computeTaxSummary($this->order);
495 539
496 - $rows = '';
497 - $rates = $this->order->orderTaxRates ?? null;
540 + if (!$summary['shouldRender']) {
541 + return '';
542 + }
498 543
499 - if ($rates && $rates->count() > 0) {
500 - foreach ($rates as $rate) {
501 - $label = $this->resolveTaxRateLabel($rate);
502 - $amount = (int) Arr::get($rate, 'total_tax', (int) Arr::get($rate, 'order_tax', 0) + (int) Arr::get($rate, 'shipping_tax', 0));
503 - $rows .= '<tr>'
504 - . '<td style="' . $labelStyle . '">' . esc_html($label) . '</td>'
505 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($amount, $currency) . '</td>'
506 - . '</tr>';
544 + $headingStyle = 'padding:8px 0 4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#94a3b8;';
545 + $mutedStyle = 'padding:3px 0;font-size:11px;color:#94a3b8;';
546 + $normalStyle = 'padding:3px 0;font-size:11px;font-weight:600;color:#525866;';
547 + $totalStyle = 'padding:6px 0 3px;font-size:11px;font-weight:700;color:#0E121B;border-top:1px solid #e2e8f0;';
548 + $valueStyle = 'text-align:right;';
549 +
550 + $rows = '';
551 +
552 + $foldedRateLines = Arr::get($summary, 'foldedRateLines', []);
553 + $isRcOrder = !empty($summary['isReverseCharge']);
554 + $rcReversedTotal = isset($summary['reversedTaxTotal']) ? (int) $summary['reversedTaxTotal'] : 0;
555 + $rcReversedValue = $rcReversedTotal > 0
556 + ? esc_html(Helper::toDecimal($rcReversedTotal))
557 + : esc_html__('Charge reversed', 'fluent-cart');
558 + $includedInPrices = (int) Arr::get($summary, 'includedInPrices', 0);
559 + $opFeeRows = Arr::get($summary, 'feeTaxLineRows', []);
560 + $taxRateLines = Arr::get($summary, 'taxRateLines', []);
561 + $productTaxRowCount = !empty($taxRateLines)
562 + ? count($taxRateLines)
563 + : (int) ($summary['inclusiveTax'] > 0) + (int) ($summary['exclusiveTax'] > 0);
564 + $rowCount = $productTaxRowCount + count($opFeeRows) + (int) ($summary['shippingTax'] > 0);
565 + $shippingTaxLines = Arr::get($summary, 'shippingTaxLines', []);
566 + $shouldShowBreakdown = !empty($taxRateLines)
567 + || !empty($shippingTaxLines)
568 + || $rowCount >= 2
569 + || ($rowCount === 1 && !($summary['payableTax'] > 0 || $summary['inclusiveTax'] > 0 || (int) Arr::get($summary, 'inclusiveFeeTax', 0) > 0));
570 +
571 + $isSimplifiedMode = ($summary['displayMode'] ?? '') === 'simplified';
572 +
573 + if ($isSimplifiedMode && !empty($summary['simpleLine'])) {
574 + $rows .= '<tr>'
575 + . '<td style="' . $normalStyle . '">' . esc_html((string) $summary['simpleLine']['label']) . '</td>'
576 + . '<td style="' . $normalStyle . $valueStyle . '">' . esc_html((string) $summary['simpleLine']['value']) . '</td>'
577 + . '</tr>';
578 +
579 + // Simplified mode: single tax line only, skip the detailed breakdown below.
580 + return $this->wrapTaxBreakdownBox($rows);
581 + }
582 +
583 + if (!empty($foldedRateLines)) {
584 + $colHeadStyle = 'width:58%;padding:3px 8px 3px 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#64748b;vertical-align:top;';
585 + $colHeadBaseStyle = 'width:24%;padding:3px 8px 3px 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#64748b;text-align:right;white-space:nowrap;vertical-align:top;';
586 + $colHeadTaxStyle = 'width:18%;padding:3px 0;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#64748b;text-align:right;white-space:nowrap;vertical-align:top;';
587 +
588 + $rows .= '<tr><td colspan="2" style="padding:5px 0 2px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#64748b;">'
589 + . esc_html__('Tax breakdown by rate', 'fluent-cart')
590 + . '</td></tr>';
591 +
592 + $nestedTable = '<table width="100%" cellpadding="0" cellspacing="0" style="border:none;border-collapse:collapse;table-layout:fixed;">';
593 + $nestedTable .= '<tr>'
594 + . '<td style="' . $colHeadStyle . '">' . esc_html__('Rate', 'fluent-cart') . '</td>'
595 + . '<td style="' . $colHeadBaseStyle . '">' . esc_html__('Taxable base', 'fluent-cart') . '</td>'
596 + . '<td style="' . $colHeadTaxStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
597 + . '</tr>';
598 +
599 + foreach ($foldedRateLines as $foldedRow) {
600 + $foldedStyle = !empty($foldedRow['inclusive']) ? $mutedStyle : $normalStyle;
601 + $foldedStyleR = $foldedStyle . $valueStyle;
602 + $nestedTable .= '<tr>'
603 + . '<td style="width:58%;' . $foldedStyle . 'padding-right:8px;white-space:normal;word-break:break-word;overflow-wrap:break-word;vertical-align:top;">' . esc_html((string) $foldedRow['label']) . '</td>'
604 + . '<td style="width:24%;' . $foldedStyleR . 'padding-right:8px;white-space:nowrap;vertical-align:top;">' . CurrencySettings::getPriceHtml((int) $foldedRow['base'], $currency) . '</td>'
605 + . '<td style="width:18%;' . $foldedStyleR . 'white-space:nowrap;vertical-align:top;">' . CurrencySettings::getPriceHtml((int) $foldedRow['tax'], $currency) . '</td>'
606 + . '</tr>';
507 607 }
608 + $nestedTable .= '</table>';
508 609
509 - return $rows;
610 + $rows .= '<tr><td colspan="2" style="padding:2px 0 0 0;">' . $nestedTable . '</td></tr>';
611 +
612 + if ($isRcOrder) {
613 + $rcReversedTotalValue = $rcReversedTotal > 0
614 + ? CurrencySettings::getPriceHtml($rcReversedTotal, $currency)
615 + : esc_html__('Charge reversed', 'fluent-cart');
616 + $rows .= '<tr>'
617 + . '<td style="' . $totalStyle . '">' . esc_html__('VAT reversed', 'fluent-cart') . '</td>'
618 + . '<td style="' . $totalStyle . $valueStyle . '">' . $rcReversedTotalValue . '</td>'
619 + . '</tr>';
620 + return $this->wrapTaxBreakdownBox($rows);
621 + }
622 +
623 + $rows .= '<tr>'
624 + . '<td style="' . $totalStyle . '">' . esc_html__('Total tax', 'fluent-cart') . '</td>'
625 + . '<td style="' . $totalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml((int) $summary['totalOrderTax'], $currency) . '</td>'
626 + . '</tr>';
627 +
628 + if ($includedInPrices > 0) {
629 + $rows .= '<tr>'
630 + . '<td style="' . $mutedStyle . '">' . esc_html__('of which included in prices', 'fluent-cart') . '</td>'
631 + . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($includedInPrices, $currency) . '</td>'
632 + . '</tr>';
633 + }
634 +
635 + if ($summary['payableTax'] > 0 && $includedInPrices > 0) {
636 + $payableStyle = $totalStyle;
637 + $rows .= '<tr>'
638 + . '<td style="' . $payableStyle . '">' . esc_html__('Payable now (added)', 'fluent-cart') . '</td>'
639 + . '<td style="' . $payableStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml((int) $summary['payableTax'], $currency) . '</td>'
640 + . '</tr>';
641 + }
642 +
643 + return $this->wrapTaxBreakdownBox($rows);
510 644 }
511 645
512 - if ($taxTotal > 0 || $shippingTax > 0) {
513 - if ($shippingTax > 0 && $taxTotal > $shippingTax) {
646 + if ($isRcOrder) {
647 + $rows .= '<tr><td colspan="2" style="' . $headingStyle . '">'
648 + . esc_html__('TAX SUMMARY', 'fluent-cart')
649 + . '</td></tr>';
650 + $rows .= '<tr>'
651 + . '<td style="' . $totalStyle . '">' . esc_html__('Tax reversed', 'fluent-cart') . '</td>'
652 + . '<td style="' . $totalStyle . $valueStyle . '">' . $rcReversedValue . '</td>'
653 + . '</tr>';
654 + return $this->wrapTaxBreakdownBox($rows);
655 + }
656 +
657 + $rows .= '<tr><td colspan="2" style="' . $headingStyle . '">'
658 + . esc_html__('TAX SUMMARY', 'fluent-cart')
659 + . '</td></tr>';
660 +
661 + if (!empty($taxRateLines) && $shouldShowBreakdown) {
662 + foreach ($taxRateLines as $taxLine) {
663 + $taxLineStyle = !empty($taxLine['inclusive']) ? $mutedStyle : $normalStyle;
514 664 $rows .= '<tr>'
515 - . '<td style="' . $labelStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
516 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxTotal - $shippingTax, $currency) . '</td>'
665 + . '<td style="' . $taxLineStyle . '">' . esc_html($taxLine['label']) . '</td>'
666 + . '<td style="' . $taxLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxLine['order_tax'], $currency) . '</td>'
517 667 . '</tr>';
668 + }
669 + }
670 +
671 + if (empty($taxRateLines) && $summary['inclusiveTax'] > 0 && $shouldShowBreakdown) {
672 + $rows .= '<tr>'
673 + . '<td style="' . $mutedStyle . '">' . esc_html__('Included in item prices', 'fluent-cart') . '</td>'
674 + . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['inclusiveTax'], $currency) . '</td>'
675 + . '</tr>';
676 + }
677 +
678 + if (empty($taxRateLines) && $summary['exclusiveTax'] > 0 && $shouldShowBreakdown) {
679 + $rows .= '<tr>'
680 + . '<td style="' . $normalStyle . '">' . esc_html__('Added on products', 'fluent-cart') . '</td>'
681 + . '<td style="' . $normalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['exclusiveTax'], $currency) . '</td>'
682 + . '</tr>';
683 + }
684 +
685 + if ($shouldShowBreakdown) {
686 + foreach ($opFeeRows as $feeRow) {
687 + $feeLineStyle = $feeRow['inclusive'] ? $mutedStyle : $normalStyle;
518 688 $rows .= '<tr>'
519 - . '<td style="' . $labelStyle . '">' . esc_html__('Shipping Tax', 'fluent-cart') . '</td>'
520 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($shippingTax, $currency) . '</td>'
689 + . '<td style="' . $feeLineStyle . '">' . esc_html($feeRow['display_label']) . '</td>'
690 + . '<td style="' . $feeLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($feeRow['tax_amount'], $currency) . '</td>'
521 691 . '</tr>';
692 + }
693 + }
694 +
695 + if ($summary['shippingTax'] > 0 && $shouldShowBreakdown) {
696 + $isShippingInclusive = TaxSummaryHelper::isShippingTaxInclusive($this->order);
697 + $shippingRowStyle = $isShippingInclusive ? $mutedStyle : $normalStyle;
698 + if (!empty($shippingTaxLines)) {
699 + foreach ($shippingTaxLines as $shippingTaxLine) {
700 + $rows .= '<tr>'
701 + . '<td style="' . $shippingRowStyle . '">' . esc_html($shippingTaxLine['label']) . '</td>'
702 + . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($shippingTaxLine['shipping_tax'], $currency) . '</td>'
703 + . '</tr>';
704 + }
522 705 } else {
706 + $shippingRowLabel = $isShippingInclusive
707 + ? esc_html__('Included in shipping prices', 'fluent-cart')
708 + : esc_html__('Added on shipping', 'fluent-cart');
523 709 $rows .= '<tr>'
524 - . '<td style="' . $labelStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
525 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxTotal, $currency) . '</td>'
710 + . '<td style="' . $shippingRowStyle . '">' . $shippingRowLabel . '</td>'
711 + . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['shippingTax'], $currency) . '</td>'
526 712 . '</tr>';
527 713 }
528 714 }
529 715
530 - return $rows;
716 + if ($summary['payableTax'] > 0) {
717 + $rows .= '<tr>'
718 + . '<td style="' . $totalStyle . '">' . esc_html__('Total payable tax', 'fluent-cart') . '</td>'
719 + . '<td style="' . $totalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['payableTax'], $currency) . '</td>'
720 + . '</tr>';
721 + }
722 +
723 + if ($summary['inclusiveTax'] > 0 || !empty($summary['inclusiveFeeTax'])) {
724 + $rows .= '<tr>'
725 + . '<td style="' . $mutedStyle . '">' . esc_html__('Total tax in this order', 'fluent-cart') . '</td>'
726 + . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['totalOrderTax'], $currency) . '</td>'
727 + . '</tr>';
728 + }
729 +
730 + return $this->wrapTaxBreakdownBox($rows);
531 731 }
532 732
733 + /**
734 + * Wrap the accumulated tax-breakdown rows in their own grey rounded box,
735 + * right-aligned to a fixed 400px column.
736 + *
737 + * The tax breakdown is the only thing that lives inside the grey box; the
738 + * surrounding Subtotal/Discount/Shipping/Fees rows (above) and Refund/Total/
739 + * Payment rows (below) are rendered as plain rows by the summary table and
740 + * stay outside this wrapper. A table-based 400px right-aligned wrapper is used
741 + * (not a div) so PDF renderers (mPDF/Dompdf) and email clients honour the
742 + * width/alignment, matching the boxed treatment on the thank-you/customer
743 + * surfaces. Consumed by the PDF receipt templates via {{order.tax_breakdown}}.
744 + *
745 + * @param string $rows Inner <tr> rows for the tax breakdown.
746 + * @return string A single <tr><td colspan="2"> cell carrying the grey box.
747 + */
748 + private function wrapTaxBreakdownBox(string $rows): string
749 + {
750 + if ($rows === '') {
751 + return '';
752 + }
753 +
754 + $boxStyle = 'background-color:rgb(249,250,251);padding:16px;border-radius:8px;';
755 +
756 + $nestedTable = '<table width="100%" cellpadding="0" cellspacing="0" style="width:100%;border:none;border-collapse:collapse;">'
757 + . $rows
758 + . '</table>';
759 +
760 + return '<tr><td colspan="2" style="padding:8px 0 0 0;">'
761 + . '<table align="right" width="400" cellpadding="0" cellspacing="0" style="width:400px;max-width:100%;border:none;border-collapse:collapse;margin:0 0 0 auto;">'
762 + . '<tr><td style="' . $boxStyle . '">' . $nestedTable . '</td></tr>'
763 + . '</table>'
764 + . '</td></tr>';
765 + }
766 +
533 767 private function resolveTaxRateLabel($orderTaxRate): string
534 768 {
535 769 $taxRate = $orderTaxRate->tax_rate ?? null;
536 770 if ($taxRate) {
@@ -587,9 +821,9 @@
587 821 }
588 822
589 823 /**
590 824 * Returns formatted buyer VAT display string, e.g. "VAT/Tax ID: XX123456".
591 - * Checks OrderMeta vat_tax_id first, then falls back to EU VAT reverse charge number.
825 + * Checks business_info first, then falls back to legacy VAT storage.
592 826 */
593 827 public function getBuyerVatDisplay(): string
594 828 {
595 829 if (!$this->order) {
@@ -595,25 +829,12 @@
595 829 if (!$this->order) {
596 830 return '';
597 831 }
598 832
599 - // Check OrderMeta vat_tax_id first (simple VAT/Tax ID)
600 - $vatMeta = OrderMeta::query()
601 - ->where('order_id', $this->order->id)
602 - ->where('meta_key', 'vat_tax_id')
603 - ->orderBy('id', 'DESC')
604 - ->first();
605 -
606 - if ($vatMeta && !empty($vatMeta->meta_value)) {
607 - return esc_html(__('VAT/Tax ID', 'fluent-cart')) . ': ' . esc_html($vatMeta->meta_value);
608 - }
609 -
610 - // Fall back to EU VAT reverse charge number
611 - $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
612 - $vatNumber = Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.vat_number', '');
613 -
833 + $vatNumber = $this->order->getCustomerTaxNumber();
614 834 if (!empty($vatNumber)) {
615 - return esc_html(__('EU VAT', 'fluent-cart')) . ': ' . esc_html($vatNumber);
835 + $label = __('VAT/Tax ID', 'fluent-cart');
836 + return esc_html($label) . ': ' . esc_html($vatNumber);
616 837 }
617 838
618 839 return '';
619 840 }
@@ -634,13 +855,110 @@
634 855 return esc_html($companyName);
635 856 }
636 857 }
637 858
638 - // Fall back to VAT reverse charge name
639 - $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
859 + return esc_html($this->order->getCustomerTaxName());
860 + }
640 861
641 - return esc_html(Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.name', ''));
862 + public function getBuyerLegalRegistrationId(): string
863 + {
864 + if (!$this->order) {
865 + return '';
866 + }
867 +
868 + if ($this->order->billing_address) {
869 + $regId = Arr::get($this->order->billing_address->meta ?? [], 'other_data.legal_registration_id', '');
870 + if (!empty($regId)) {
871 + return esc_html($regId);
872 + }
873 + }
874 +
875 + return esc_html(Arr::get($this->order->getBusinessInfo(), 'legal_registration_id', ''));
642 876 }
643 877
644 -}
878 + public function getBuyerReverseChargeDeclaration(): string
879 + {
880 + if (!$this->order) {
881 + return '';
882 + }
645 883
884 + return esc_html(Arr::get($this->order->getBusinessInfo(), 'reverse_charge_declaration', ''));
885 + }
646 886
887 + // -------------------------------------------------------------------------
888 + // Store business info — reads from snapshotted fct_order_meta[store_business_info]
889 + // with live StoreSettings fallback for orders placed before this feature.
890 + // -------------------------------------------------------------------------
891 +
892 + private function getStoreBusinessField(string $field): string
893 + {
894 + if (!$this->order) {
895 + return '';
896 + }
897 + $snapshot = $this->order->getMeta('store_business_info', false);
898 + if ($snapshot !== false && isset($snapshot[$field])) {
899 + return (string) $snapshot[$field];
900 + }
901 + return (string) $this->storeSettings->get($field, '');
902 + }
903 +
904 + public function getStoreCompanyName(): string
905 + {
906 + return esc_html($this->getStoreBusinessField('company_name'));
907 + }
908 +
909 + public function getStoreCompanyDisplay(): string
910 + {
911 + $val = $this->getStoreBusinessField('company_name');
912 + if (empty($val)) {
913 + return '';
914 + }
915 + /* translators: %1$s: store company name */
916 + return esc_html(sprintf(__('Company: %1$s', 'fluent-cart'), $val));
917 + }
918 +
919 + public function getStoreLegalRegistrationId(): string
920 + {
921 + return esc_html($this->getStoreBusinessField('legal_registration_id'));
922 + }
923 +
924 + public function getStoreLegalRegistrationDisplay(): string
925 + {
926 + $val = $this->getStoreBusinessField('legal_registration_id');
927 + if (empty($val)) {
928 + return '';
929 + }
930 + /* translators: %1$s: store legal registration ID */
931 + return esc_html(sprintf(__('Reg. ID: %1$s', 'fluent-cart'), $val));
932 + }
933 +
934 + public function getStoreSellerVatId(): string
935 + {
936 + return esc_html($this->getStoreBusinessField('seller_vat_id'));
937 + }
938 +
939 + public function getStoreSellerVatDisplay(): string
940 + {
941 + $val = $this->getStoreBusinessField('seller_vat_id');
942 + if (empty($val)) {
943 + return '';
944 + }
945 + /* translators: %1$s: store seller VAT ID */
946 + return esc_html(sprintf(__('VAT ID: %1$s', 'fluent-cart'), $val));
947 + }
948 +
949 + public function getStoreSellerTaxId(): string
950 + {
951 + return esc_html($this->getStoreBusinessField('seller_tax_id'));
952 + }
953 +
954 + public function getStoreTaxDisplay(): string
955 + {
956 + $val = $this->getStoreBusinessField('seller_tax_id');
957 + if (empty($val)) {
958 + return '';
959 + }
960 + /* translators: %1$s: store seller tax ID */
961 + return esc_html(sprintf(__('Tax ID: %1$s', 'fluent-cart'), $val));
962 + }
963 +
964 +}