PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 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 All 50 releases
← All changes | app/Services/ShortCodeParser/Parsers/OrderParser.php +406 -57 1.3.28 → 1.7.0 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
@@ -53,12 +55,25 @@
53 55 // ];
54 56
55 57 protected array $methodMap = [
56 58 'item_count' => 'getItemCount',
59 + 'review_url' => 'getReviewUrl',
57 60 'is_digital' => 'getIsDigital',
58 - 'store_vat_display' => 'getStoreVatDisplay',
59 - 'buyer_vat_display' => 'getBuyerVatDisplay',
60 - 'buyer_company_name' => 'getBuyerCompanyName',
61 + 'store_vat_display' => 'getStoreVatDisplay',
62 + 'store_company_name' => 'getStoreCompanyName',
63 + 'store_company_display' => 'getStoreCompanyDisplay',
64 + 'store_legal_registration_id' => 'getStoreLegalRegistrationId',
65 + 'store_legal_registration_display' => 'getStoreLegalRegistrationDisplay',
66 + 'store_seller_vat_id' => 'getStoreSellerVatId',
67 + 'store_seller_vat_display' => 'getStoreSellerVatDisplay',
68 + 'store_seller_tax_id' => 'getStoreSellerTaxId',
69 + 'store_tax_display' => 'getStoreTaxDisplay',
70 + 'buyer_vat_display' => 'getBuyerVatDisplay',
71 + 'buyer_company_name' => 'getBuyerCompanyName',
72 + 'buyer_legal_registration_id' => 'getBuyerLegalRegistrationId',
73 + 'buyer_reverse_charge_declaration' => 'getBuyerReverseChargeDeclaration',
74 + 'tax_breakdown' => 'getTaxBreakdown',
75 + 'fee_lines' => 'getFeeLines',
61 76 ];
62 77
63 78 protected array $attributeMap = [
64 79 'id' => 'order.id',
@@ -91,14 +106,12 @@
91 106 if (in_array($accessor, ['updated_at', 'created_at'])) {
92 107 $date = Arr::get($this->data, $this->attributeMap[$accessor]);
93 108 $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp();
94 109
95 - $date = wp_date(
96 - get_option('date_format'),
97 - $timestamp,
98 - new \DateTimeZone($this->orderTz)
99 - );
100 -
110 + // Through DateFormatter so the store's format and timezone settings
111 + // reach this too; orderTz is only consulted on the Browser source.
112 + $date = DateFormatter::format($timestamp, false, $this->orderTz);
113 +
101 114 return Helper::translateNumber($date);
102 115 }
103 116
104 117 // Handle _formatted suffix for cent columns (e.g. total_amount_formatted)
@@ -348,8 +361,47 @@
348 361
349 362 return is_user_logged_in() ? $orderLink : wp_login_url($orderLink);
350 363 }
351 364
365 + /**
366 + * The public "review your order" page for this order.
367 + *
368 + * Carries the order uuid, which is what lets the link work for a
369 + * guest-checkout buyer with no account to sign into — so, unlike
370 + * getCustomerOrderLink(), it is never wrapped in wp_login_url().
371 + *
372 + * Drop {{order.review_url}} into any order email template to invite a
373 + * review. Best paired with a notification that fires after fulfilment;
374 + * the page only resolves once the order has actually completed.
375 + */
376 + public function getReviewUrl($accessor = null, $code = null)
377 + {
378 + $orderHash = (string) Arr::get($this->order, 'uuid', '');
379 +
380 + if ($orderHash !== '') {
381 + return Order::reviewUrlForHash($orderHash);
382 + }
383 +
384 + $orderId = Arr::get($this->order, 'id');
385 +
386 + if (empty($orderId)) {
387 + return $code;
388 + }
389 +
390 + $order = Order::query()->find($orderId);
391 +
392 + return $order instanceof Order ? $order->getOrderReviewUrl() : $code;
393 + }
394 +
395 + public function getPaymentLink($accessor = null, $code = null)
396 + {
397 + if (empty($this->order)) {
398 + return $code;
399 + }
400 +
401 + return PaymentHelper::getCustomPaymentLink(Arr::get($this->order, 'uuid'));
402 + }
403 +
352 404 public function getAdminOrderLink($accessor, $code = null)
353 405 {
354 406 if (empty($this->order)) {
355 407 return $code;
@@ -479,8 +531,35 @@
479 531
480 532 return ucwords(str_replace(['_', '-'], ' ', $slug));
481 533 }
482 534
535 + public function getFeeLines(): string
536 + {
537 + if (!$this->order) {
538 + return '';
539 + }
540 +
541 + $currency = (string) Arr::get($this->order, 'currency', '');
542 + $feeItems = $this->order->feeItems()->get();
543 +
544 + if ($feeItems->isEmpty()) {
545 + return '';
546 + }
547 +
548 + $rowStyle = 'padding:3px 0;font-size:11px;font-weight:600;color:#525866;';
549 + $valueStyle = 'text-align:right;';
550 + $rows = '';
551 +
552 + foreach ($feeItems as $feeItem) {
553 + $rows .= '<tr>'
554 + . '<td style="' . $rowStyle . '">' . esc_html((string) $feeItem->title) . '</td>'
555 + . '<td style="' . $rowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml((int) $feeItem->subtotal, $currency) . '</td>'
556 + . '</tr>';
557 + }
558 +
559 + return $rows;
560 + }
561 +
483 562 public function getTaxBreakdown(): string
484 563 {
485 564 if (!$this->order) {
486 565 return '';
@@ -485,52 +564,238 @@
485 564 if (!$this->order) {
486 565 return '';
487 566 }
488 567
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;';
568 + $currency = (string) Arr::get($this->order, 'currency', '');
569 + $summary = TaxSummaryHelper::computeTaxSummary($this->order);
495 570
496 - $rows = '';
497 - $rates = $this->order->orderTaxRates ?? null;
571 + if (!$summary['shouldRender']) {
572 + return '';
573 + }
498 574
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>';
575 + $headingStyle = 'padding:8px 0 4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#94a3b8;';
576 + $mutedStyle = 'padding:3px 0;font-size:11px;color:#94a3b8;';
577 + $normalStyle = 'padding:3px 0;font-size:11px;font-weight:600;color:#525866;';
578 + $totalStyle = 'padding:6px 0 3px;font-size:11px;font-weight:700;color:#0E121B;border-top:1px solid #e2e8f0;';
579 + $valueStyle = 'text-align:right;';
580 +
581 + $rows = '';
582 +
583 + $foldedRateLines = Arr::get($summary, 'foldedRateLines', []);
584 + $isRcOrder = !empty($summary['isReverseCharge']);
585 + $rcReversedTotal = isset($summary['reversedTaxTotal']) ? (int) $summary['reversedTaxTotal'] : 0;
586 + $rcReversedValue = $rcReversedTotal > 0
587 + ? esc_html(Helper::toDecimal($rcReversedTotal))
588 + : esc_html__('Charge reversed', 'fluent-cart');
589 + $includedInPrices = (int) Arr::get($summary, 'includedInPrices', 0);
590 + $opFeeRows = Arr::get($summary, 'feeTaxLineRows', []);
591 + $taxRateLines = Arr::get($summary, 'taxRateLines', []);
592 + $productTaxRowCount = !empty($taxRateLines)
593 + ? count($taxRateLines)
594 + : (int) ($summary['inclusiveTax'] > 0) + (int) ($summary['exclusiveTax'] > 0);
595 + $rowCount = $productTaxRowCount + count($opFeeRows) + (int) ($summary['shippingTax'] > 0);
596 + $shippingTaxLines = Arr::get($summary, 'shippingTaxLines', []);
597 + $shouldShowBreakdown = !empty($taxRateLines)
598 + || !empty($shippingTaxLines)
599 + || $rowCount >= 2
600 + || ($rowCount === 1 && !($summary['payableTax'] > 0 || $summary['inclusiveTax'] > 0 || (int) Arr::get($summary, 'inclusiveFeeTax', 0) > 0));
601 +
602 + $isSimplifiedMode = ($summary['displayMode'] ?? '') === 'simplified';
603 +
604 + if ($isSimplifiedMode && !empty($summary['simpleLine'])) {
605 + $rows .= '<tr>'
606 + . '<td style="' . $normalStyle . '">' . esc_html((string) $summary['simpleLine']['label']) . '</td>'
607 + . '<td style="' . $normalStyle . $valueStyle . '">' . esc_html((string) $summary['simpleLine']['value']) . '</td>'
608 + . '</tr>';
609 +
610 + // Simplified mode: single tax line only, skip the detailed breakdown below.
611 + return $this->wrapTaxBreakdownBox($rows);
612 + }
613 +
614 + if (!empty($foldedRateLines)) {
615 + $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;';
616 + $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;';
617 + $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;';
618 +
619 + $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;">'
620 + . esc_html__('Tax breakdown by rate', 'fluent-cart')
621 + . '</td></tr>';
622 +
623 + $nestedTable = '<table width="100%" cellpadding="0" cellspacing="0" style="border:none;border-collapse:collapse;table-layout:fixed;">';
624 + $nestedTable .= '<tr>'
625 + . '<td style="' . $colHeadStyle . '">' . esc_html__('Rate', 'fluent-cart') . '</td>'
626 + . '<td style="' . $colHeadBaseStyle . '">' . esc_html__('Taxable base', 'fluent-cart') . '</td>'
627 + . '<td style="' . $colHeadTaxStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
628 + . '</tr>';
629 +
630 + foreach ($foldedRateLines as $foldedRow) {
631 + $foldedStyle = !empty($foldedRow['inclusive']) ? $mutedStyle : $normalStyle;
632 + $foldedStyleR = $foldedStyle . $valueStyle;
633 + $nestedTable .= '<tr>'
634 + . '<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>'
635 + . '<td style="width:24%;' . $foldedStyleR . 'padding-right:8px;white-space:nowrap;vertical-align:top;">' . CurrencySettings::getPriceHtml((int) $foldedRow['base'], $currency) . '</td>'
636 + . '<td style="width:18%;' . $foldedStyleR . 'white-space:nowrap;vertical-align:top;">' . CurrencySettings::getPriceHtml((int) $foldedRow['tax'], $currency) . '</td>'
637 + . '</tr>';
507 638 }
639 + $nestedTable .= '</table>';
508 640
509 - return $rows;
641 + $rows .= '<tr><td colspan="2" style="padding:2px 0 0 0;">' . $nestedTable . '</td></tr>';
642 +
643 + if ($isRcOrder) {
644 + $rcReversedTotalValue = $rcReversedTotal > 0
645 + ? CurrencySettings::getPriceHtml($rcReversedTotal, $currency)
646 + : esc_html__('Charge reversed', 'fluent-cart');
647 + $rows .= '<tr>'
648 + . '<td style="' . $totalStyle . '">' . esc_html__('VAT reversed', 'fluent-cart') . '</td>'
649 + . '<td style="' . $totalStyle . $valueStyle . '">' . $rcReversedTotalValue . '</td>'
650 + . '</tr>';
651 + return $this->wrapTaxBreakdownBox($rows);
652 + }
653 +
654 + $rows .= '<tr>'
655 + . '<td style="' . $totalStyle . '">' . esc_html__('Total tax', 'fluent-cart') . '</td>'
656 + . '<td style="' . $totalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml((int) $summary['totalOrderTax'], $currency) . '</td>'
657 + . '</tr>';
658 +
659 + if ($includedInPrices > 0) {
660 + $rows .= '<tr>'
661 + . '<td style="' . $mutedStyle . '">' . esc_html__('of which included in prices', 'fluent-cart') . '</td>'
662 + . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($includedInPrices, $currency) . '</td>'
663 + . '</tr>';
664 + }
665 +
666 + if ($summary['payableTax'] > 0 && $includedInPrices > 0) {
667 + $payableStyle = $totalStyle;
668 + $rows .= '<tr>'
669 + . '<td style="' . $payableStyle . '">' . esc_html__('Payable now (added)', 'fluent-cart') . '</td>'
670 + . '<td style="' . $payableStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml((int) $summary['payableTax'], $currency) . '</td>'
671 + . '</tr>';
672 + }
673 +
674 + return $this->wrapTaxBreakdownBox($rows);
510 675 }
511 676
512 - if ($taxTotal > 0 || $shippingTax > 0) {
513 - if ($shippingTax > 0 && $taxTotal > $shippingTax) {
677 + if ($isRcOrder) {
678 + $rows .= '<tr><td colspan="2" style="' . $headingStyle . '">'
679 + . esc_html__('TAX SUMMARY', 'fluent-cart')
680 + . '</td></tr>';
681 + $rows .= '<tr>'
682 + . '<td style="' . $totalStyle . '">' . esc_html__('Tax reversed', 'fluent-cart') . '</td>'
683 + . '<td style="' . $totalStyle . $valueStyle . '">' . $rcReversedValue . '</td>'
684 + . '</tr>';
685 + return $this->wrapTaxBreakdownBox($rows);
686 + }
687 +
688 + $rows .= '<tr><td colspan="2" style="' . $headingStyle . '">'
689 + . esc_html__('TAX SUMMARY', 'fluent-cart')
690 + . '</td></tr>';
691 +
692 + if (!empty($taxRateLines) && $shouldShowBreakdown) {
693 + foreach ($taxRateLines as $taxLine) {
694 + $taxLineStyle = !empty($taxLine['inclusive']) ? $mutedStyle : $normalStyle;
514 695 $rows .= '<tr>'
515 - . '<td style="' . $labelStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
516 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxTotal - $shippingTax, $currency) . '</td>'
696 + . '<td style="' . $taxLineStyle . '">' . esc_html($taxLine['label']) . '</td>'
697 + . '<td style="' . $taxLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxLine['order_tax'], $currency) . '</td>'
517 698 . '</tr>';
699 + }
700 + }
701 +
702 + if (empty($taxRateLines) && $summary['inclusiveTax'] > 0 && $shouldShowBreakdown) {
703 + $rows .= '<tr>'
704 + . '<td style="' . $mutedStyle . '">' . esc_html__('Included in item prices', 'fluent-cart') . '</td>'
705 + . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['inclusiveTax'], $currency) . '</td>'
706 + . '</tr>';
707 + }
708 +
709 + if (empty($taxRateLines) && $summary['exclusiveTax'] > 0 && $shouldShowBreakdown) {
710 + $rows .= '<tr>'
711 + . '<td style="' . $normalStyle . '">' . esc_html__('Added on products', 'fluent-cart') . '</td>'
712 + . '<td style="' . $normalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['exclusiveTax'], $currency) . '</td>'
713 + . '</tr>';
714 + }
715 +
716 + if ($shouldShowBreakdown) {
717 + foreach ($opFeeRows as $feeRow) {
718 + $feeLineStyle = $feeRow['inclusive'] ? $mutedStyle : $normalStyle;
518 719 $rows .= '<tr>'
519 - . '<td style="' . $labelStyle . '">' . esc_html__('Shipping Tax', 'fluent-cart') . '</td>'
520 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($shippingTax, $currency) . '</td>'
720 + . '<td style="' . $feeLineStyle . '">' . esc_html($feeRow['display_label']) . '</td>'
721 + . '<td style="' . $feeLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($feeRow['tax_amount'], $currency) . '</td>'
521 722 . '</tr>';
723 + }
724 + }
725 +
726 + if ($summary['shippingTax'] > 0 && $shouldShowBreakdown) {
727 + $isShippingInclusive = TaxSummaryHelper::isShippingTaxInclusive($this->order);
728 + $shippingRowStyle = $isShippingInclusive ? $mutedStyle : $normalStyle;
729 + if (!empty($shippingTaxLines)) {
730 + foreach ($shippingTaxLines as $shippingTaxLine) {
731 + $rows .= '<tr>'
732 + . '<td style="' . $shippingRowStyle . '">' . esc_html($shippingTaxLine['label']) . '</td>'
733 + . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($shippingTaxLine['shipping_tax'], $currency) . '</td>'
734 + . '</tr>';
735 + }
522 736 } else {
737 + $shippingRowLabel = $isShippingInclusive
738 + ? esc_html__('Included in shipping prices', 'fluent-cart')
739 + : esc_html__('Added on shipping', 'fluent-cart');
523 740 $rows .= '<tr>'
524 - . '<td style="' . $labelStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
525 - . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxTotal, $currency) . '</td>'
741 + . '<td style="' . $shippingRowStyle . '">' . $shippingRowLabel . '</td>'
742 + . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['shippingTax'], $currency) . '</td>'
526 743 . '</tr>';
527 744 }
528 745 }
529 746
530 - return $rows;
747 + if ($summary['payableTax'] > 0) {
748 + $rows .= '<tr>'
749 + . '<td style="' . $totalStyle . '">' . esc_html__('Total payable tax', 'fluent-cart') . '</td>'
750 + . '<td style="' . $totalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['payableTax'], $currency) . '</td>'
751 + . '</tr>';
752 + }
753 +
754 + if ($summary['inclusiveTax'] > 0 || !empty($summary['inclusiveFeeTax'])) {
755 + $rows .= '<tr>'
756 + . '<td style="' . $mutedStyle . '">' . esc_html__('Total tax in this order', 'fluent-cart') . '</td>'
757 + . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['totalOrderTax'], $currency) . '</td>'
758 + . '</tr>';
759 + }
760 +
761 + return $this->wrapTaxBreakdownBox($rows);
531 762 }
532 763
764 + /**
765 + * Wrap the accumulated tax-breakdown rows in their own grey rounded box,
766 + * right-aligned to a fixed 400px column.
767 + *
768 + * The tax breakdown is the only thing that lives inside the grey box; the
769 + * surrounding Subtotal/Discount/Shipping/Fees rows (above) and Refund/Total/
770 + * Payment rows (below) are rendered as plain rows by the summary table and
771 + * stay outside this wrapper. A table-based 400px right-aligned wrapper is used
772 + * (not a div) so PDF renderers (mPDF/Dompdf) and email clients honour the
773 + * width/alignment, matching the boxed treatment on the thank-you/customer
774 + * surfaces. Consumed by the PDF receipt templates via {{order.tax_breakdown}}.
775 + *
776 + * @param string $rows Inner <tr> rows for the tax breakdown.
777 + * @return string A single <tr><td colspan="2"> cell carrying the grey box.
778 + */
779 + private function wrapTaxBreakdownBox(string $rows): string
780 + {
781 + if ($rows === '') {
782 + return '';
783 + }
784 +
785 + $boxStyle = 'background-color:rgb(249,250,251);padding:16px;border-radius:8px;';
786 +
787 + $nestedTable = '<table width="100%" cellpadding="0" cellspacing="0" style="width:100%;border:none;border-collapse:collapse;">'
788 + . $rows
789 + . '</table>';
790 +
791 + return '<tr><td colspan="2" style="padding:8px 0 0 0;">'
792 + . '<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;">'
793 + . '<tr><td style="' . $boxStyle . '">' . $nestedTable . '</td></tr>'
794 + . '</table>'
795 + . '</td></tr>';
796 + }
797 +
533 798 private function resolveTaxRateLabel($orderTaxRate): string
534 799 {
535 800 $taxRate = $orderTaxRate->tax_rate ?? null;
536 801 if ($taxRate) {
@@ -587,9 +852,9 @@
587 852 }
588 853
589 854 /**
590 855 * 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.
856 + * Checks business_info first, then falls back to legacy VAT storage.
592 857 */
593 858 public function getBuyerVatDisplay(): string
594 859 {
595 860 if (!$this->order) {
@@ -595,25 +860,12 @@
595 860 if (!$this->order) {
596 861 return '';
597 862 }
598 863
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 -
864 + $vatNumber = $this->order->getCustomerTaxNumber();
614 865 if (!empty($vatNumber)) {
615 - return esc_html(__('EU VAT', 'fluent-cart')) . ': ' . esc_html($vatNumber);
866 + $label = __('VAT/Tax ID', 'fluent-cart');
867 + return esc_html($label) . ': ' . esc_html($vatNumber);
616 868 }
617 869
618 870 return '';
619 871 }
@@ -634,13 +886,110 @@
634 886 return esc_html($companyName);
635 887 }
636 888 }
637 889
638 - // Fall back to VAT reverse charge name
639 - $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
890 + return esc_html($this->order->getCustomerTaxName());
891 + }
640 892
641 - return esc_html(Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.name', ''));
893 + public function getBuyerLegalRegistrationId(): string
894 + {
895 + if (!$this->order) {
896 + return '';
897 + }
898 +
899 + if ($this->order->billing_address) {
900 + $regId = Arr::get($this->order->billing_address->meta ?? [], 'other_data.legal_registration_id', '');
901 + if (!empty($regId)) {
902 + return esc_html($regId);
903 + }
904 + }
905 +
906 + return esc_html(Arr::get($this->order->getBusinessInfo(), 'legal_registration_id', ''));
642 907 }
643 908
644 -}
909 + public function getBuyerReverseChargeDeclaration(): string
910 + {
911 + if (!$this->order) {
912 + return '';
913 + }
645 914
915 + return esc_html(Arr::get($this->order->getBusinessInfo(), 'reverse_charge_declaration', ''));
916 + }
646 917
918 + // -------------------------------------------------------------------------
919 + // Store business info — reads from snapshotted fct_order_meta[store_business_info]
920 + // with live StoreSettings fallback for orders placed before this feature.
921 + // -------------------------------------------------------------------------
922 +
923 + private function getStoreBusinessField(string $field): string
924 + {
925 + if (!$this->order) {
926 + return '';
927 + }
928 + $snapshot = $this->order->getMeta('store_business_info', false);
929 + if ($snapshot !== false && isset($snapshot[$field])) {
930 + return (string) $snapshot[$field];
931 + }
932 + return (string) $this->storeSettings->get($field, '');
933 + }
934 +
935 + public function getStoreCompanyName(): string
936 + {
937 + return esc_html($this->getStoreBusinessField('company_name'));
938 + }
939 +
940 + public function getStoreCompanyDisplay(): string
941 + {
942 + $val = $this->getStoreBusinessField('company_name');
943 + if (empty($val)) {
944 + return '';
945 + }
946 + /* translators: %1$s: store company name */
947 + return esc_html(sprintf(__('Company: %1$s', 'fluent-cart'), $val));
948 + }
949 +
950 + public function getStoreLegalRegistrationId(): string
951 + {
952 + return esc_html($this->getStoreBusinessField('legal_registration_id'));
953 + }
954 +
955 + public function getStoreLegalRegistrationDisplay(): string
956 + {
957 + $val = $this->getStoreBusinessField('legal_registration_id');
958 + if (empty($val)) {
959 + return '';
960 + }
961 + /* translators: %1$s: store legal registration ID */
962 + return esc_html(sprintf(__('Reg. ID: %1$s', 'fluent-cart'), $val));
963 + }
964 +
965 + public function getStoreSellerVatId(): string
966 + {
967 + return esc_html($this->getStoreBusinessField('seller_vat_id'));
968 + }
969 +
970 + public function getStoreSellerVatDisplay(): string
971 + {
972 + $val = $this->getStoreBusinessField('seller_vat_id');
973 + if (empty($val)) {
974 + return '';
975 + }
976 + /* translators: %1$s: store seller VAT ID */
977 + return esc_html(sprintf(__('VAT ID: %1$s', 'fluent-cart'), $val));
978 + }
979 +
980 + public function getStoreSellerTaxId(): string
981 + {
982 + return esc_html($this->getStoreBusinessField('seller_tax_id'));
983 + }
984 +
985 + public function getStoreTaxDisplay(): string
986 + {
987 + $val = $this->getStoreBusinessField('seller_tax_id');
988 + if (empty($val)) {
989 + return '';
990 + }
991 + /* translators: %1$s: store seller tax ID */
992 + return esc_html(sprintf(__('Tax ID: %1$s', 'fluent-cart'), $val));
993 + }
994 +
995 +}