← All changes
|
app/Services/ShortCodeParser/Parsers/OrderParser.php
+452
-31
1.3.23
→
1.6.5
View file →
| @@ -6,13 +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; |
| 11 | +use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager; | |
| 12 | 12 | use FluentCart\App\Services\DateTime\DateTime; |
| 13 | +use FluentCart\App\Services\DateTime\DateFormatter; | |
| 14 | +use FluentCart\App\Services\Payments\PaymentHelper; | |
| 13 | 15 | use FluentCart\App\Services\Payments\PaymentReceipt; |
| 14 | 16 | use FluentCart\App\Services\TemplateService; |
| 17 | +use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper; | |
| 15 | 18 | use FluentCart\Framework\Support\Arr; |
| 16 | 19 | use FluentCart\Framework\Support\Str; |
| 17 | 20 | use FluentCartPro\App\Modules\Licensing\Models\License; |
| 18 | 21 | |
| @@ -53,11 +56,23 @@ | ||
| 53 | 56 | |
| 54 | 57 | protected array $methodMap = [ |
| 55 | 58 | 'item_count' => 'getItemCount', |
| 56 | 59 | 'is_digital' => 'getIsDigital', |
| 57 | - 'store_vat_display' => 'getStoreVatDisplay', | |
| 58 | - 'buyer_vat_display' => 'getBuyerVatDisplay', | |
| 59 | - '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', | |
| 60 | 75 | ]; |
| 61 | 76 | |
| 62 | 77 | protected array $attributeMap = [ |
| 63 | 78 | 'id' => 'order.id', |
| @@ -90,14 +105,12 @@ | ||
| 90 | 105 | if (in_array($accessor, ['updated_at', 'created_at'])) { |
| 91 | 106 | $date = Arr::get($this->data, $this->attributeMap[$accessor]); |
| 92 | 107 | $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp(); |
| 93 | 108 | |
| 94 | - $date = wp_date( | |
| 95 | - get_option('date_format'), | |
| 96 | - $timestamp, | |
| 97 | - new \DateTimeZone($this->orderTz) | |
| 98 | - ); | |
| 99 | - | |
| 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 | + | |
| 100 | 113 | return Helper::translateNumber($date); |
| 101 | 114 | } |
| 102 | 115 | |
| 103 | 116 | // Handle _formatted suffix for cent columns (e.g. total_amount_formatted) |
| @@ -347,8 +360,17 @@ | ||
| 347 | 360 | |
| 348 | 361 | return is_user_logged_in() ? $orderLink : wp_login_url($orderLink); |
| 349 | 362 | } |
| 350 | 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 | + | |
| 351 | 373 | public function getAdminOrderLink($accessor, $code = null) |
| 352 | 374 | { |
| 353 | 375 | if (empty($this->order)) { |
| 354 | 376 | return $code; |
| @@ -449,8 +471,323 @@ | ||
| 449 | 471 | |
| 450 | 472 | return '0'; |
| 451 | 473 | } |
| 452 | 474 | |
| 475 | + public function getPaymentMethodTitle(): string | |
| 476 | + { | |
| 477 | + if (!$this->order) { | |
| 478 | + return ''; | |
| 479 | + } | |
| 480 | + | |
| 481 | + $title = (string) Arr::get($this->order, 'payment_method_title', ''); | |
| 482 | + if ($title !== '') { | |
| 483 | + return $title; | |
| 484 | + } | |
| 485 | + | |
| 486 | + $slug = (string) Arr::get($this->order, 'payment_method', ''); | |
| 487 | + if ($slug === '') { | |
| 488 | + return ''; | |
| 489 | + } | |
| 490 | + | |
| 491 | + if (class_exists(GatewayManager::class)) { | |
| 492 | + $gateway = GatewayManager::getInstance($slug); | |
| 493 | + if ($gateway) { | |
| 494 | + $gatewayTitle = (string) $gateway->getMeta('title'); | |
| 495 | + if ($gatewayTitle !== '') { | |
| 496 | + return $gatewayTitle; | |
| 497 | + } | |
| 498 | + } | |
| 499 | + } | |
| 500 | + | |
| 501 | + return ucwords(str_replace(['_', '-'], ' ', $slug)); | |
| 502 | + } | |
| 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 | + | |
| 531 | + public function getTaxBreakdown(): string | |
| 532 | + { | |
| 533 | + if (!$this->order) { | |
| 534 | + return ''; | |
| 535 | + } | |
| 536 | + | |
| 537 | + $currency = (string) Arr::get($this->order, 'currency', ''); | |
| 538 | + $summary = TaxSummaryHelper::computeTaxSummary($this->order); | |
| 539 | + | |
| 540 | + if (!$summary['shouldRender']) { | |
| 541 | + return ''; | |
| 542 | + } | |
| 543 | + | |
| 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>'; | |
| 607 | + } | |
| 608 | + $nestedTable .= '</table>'; | |
| 609 | + | |
| 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); | |
| 644 | + } | |
| 645 | + | |
| 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; | |
| 664 | + $rows .= '<tr>' | |
| 665 | + . '<td style="' . $taxLineStyle . '">' . esc_html($taxLine['label']) . '</td>' | |
| 666 | + . '<td style="' . $taxLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxLine['order_tax'], $currency) . '</td>' | |
| 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; | |
| 688 | + $rows .= '<tr>' | |
| 689 | + . '<td style="' . $feeLineStyle . '">' . esc_html($feeRow['display_label']) . '</td>' | |
| 690 | + . '<td style="' . $feeLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($feeRow['tax_amount'], $currency) . '</td>' | |
| 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 | + } | |
| 705 | + } else { | |
| 706 | + $shippingRowLabel = $isShippingInclusive | |
| 707 | + ? esc_html__('Included in shipping prices', 'fluent-cart') | |
| 708 | + : esc_html__('Added on shipping', 'fluent-cart'); | |
| 709 | + $rows .= '<tr>' | |
| 710 | + . '<td style="' . $shippingRowStyle . '">' . $shippingRowLabel . '</td>' | |
| 711 | + . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['shippingTax'], $currency) . '</td>' | |
| 712 | + . '</tr>'; | |
| 713 | + } | |
| 714 | + } | |
| 715 | + | |
| 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); | |
| 731 | + } | |
| 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 | + | |
| 767 | + private function resolveTaxRateLabel($orderTaxRate): string | |
| 768 | + { | |
| 769 | + $taxRate = $orderTaxRate->tax_rate ?? null; | |
| 770 | + if ($taxRate) { | |
| 771 | + $name = (string) Arr::get($taxRate, 'name', ''); | |
| 772 | + $rate = Arr::get($taxRate, 'rate'); | |
| 773 | + if ($name !== '' && is_numeric($rate)) { | |
| 774 | + $rate = (string) $rate; | |
| 775 | + | |
| 776 | + if (strpos($rate, '.') !== false) { | |
| 777 | + $rate = rtrim(rtrim($rate, '0'), '.'); | |
| 778 | + } | |
| 779 | + | |
| 780 | + return $name . ' (' . $rate . '%)'; | |
| 781 | + } | |
| 782 | + if ($name !== '') { | |
| 783 | + return $name; | |
| 784 | + } | |
| 785 | + } | |
| 786 | + | |
| 787 | + return __('Tax', 'fluent-cart'); | |
| 788 | + } | |
| 789 | + | |
| 453 | 790 | public function getSubscriptions() |
| 454 | 791 | { |
| 455 | 792 | return ''; |
| 456 | 793 | } |
| @@ -484,9 +821,9 @@ | ||
| 484 | 821 | } |
| 485 | 822 | |
| 486 | 823 | /** |
| 487 | 824 | * Returns formatted buyer VAT display string, e.g. "VAT/Tax ID: XX123456". |
| 488 | - * 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. | |
| 489 | 826 | */ |
| 490 | 827 | public function getBuyerVatDisplay(): string |
| 491 | 828 | { |
| 492 | 829 | if (!$this->order) { |
| @@ -492,25 +829,12 @@ | ||
| 492 | 829 | if (!$this->order) { |
| 493 | 830 | return ''; |
| 494 | 831 | } |
| 495 | 832 | |
| 496 | - // Check OrderMeta vat_tax_id first (simple VAT/Tax ID) | |
| 497 | - $vatMeta = OrderMeta::query() | |
| 498 | - ->where('order_id', $this->order->id) | |
| 499 | - ->where('meta_key', 'vat_tax_id') | |
| 500 | - ->orderBy('id', 'DESC') | |
| 501 | - ->first(); | |
| 502 | - | |
| 503 | - if ($vatMeta && !empty($vatMeta->meta_value)) { | |
| 504 | - return esc_html(__('VAT/Tax ID', 'fluent-cart')) . ': ' . esc_html($vatMeta->meta_value); | |
| 505 | - } | |
| 506 | - | |
| 507 | - // Fall back to EU VAT reverse charge number | |
| 508 | - $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null; | |
| 509 | - $vatNumber = Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.vat_number', ''); | |
| 510 | - | |
| 833 | + $vatNumber = $this->order->getCustomerTaxNumber(); | |
| 511 | 834 | if (!empty($vatNumber)) { |
| 512 | - 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); | |
| 513 | 837 | } |
| 514 | 838 | |
| 515 | 839 | return ''; |
| 516 | 840 | } |
| @@ -531,13 +855,110 @@ | ||
| 531 | 855 | return esc_html($companyName); |
| 532 | 856 | } |
| 533 | 857 | } |
| 534 | 858 | |
| 535 | - // Fall back to VAT reverse charge name | |
| 536 | - $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null; | |
| 859 | + return esc_html($this->order->getCustomerTaxName()); | |
| 860 | + } | |
| 537 | 861 | |
| 538 | - 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', '')); | |
| 539 | 876 | } |
| 540 | 877 | |
| 541 | -} | |
| 878 | + public function getBuyerReverseChargeDeclaration(): string | |
| 879 | + { | |
| 880 | + if (!$this->order) { | |
| 881 | + return ''; | |
| 882 | + } | |
| 542 | 883 | |
| 884 | + return esc_html(Arr::get($this->order->getBusinessInfo(), 'reverse_charge_declaration', '')); | |
| 885 | + } | |
| 543 | 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 | +} | |