← All changes
|
app/Services/ShortCodeParser/Parsers/OrderParser.php
+517
-9
1.3.19
→
1.6.5
View file →
| @@ -7,11 +7,15 @@ | ||
| 7 | 7 | use FluentCart\App\Helpers\Helper; |
| 8 | 8 | use FluentCart\App\Helpers\Status; |
| 9 | 9 | use FluentCart\App\Models\Order; |
| 10 | 10 | use FluentCart\App\Models\Subscription; |
| 11 | +use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager; | |
| 11 | 12 | use FluentCart\App\Services\DateTime\DateTime; |
| 13 | +use FluentCart\App\Services\DateTime\DateFormatter; | |
| 14 | +use FluentCart\App\Services\Payments\PaymentHelper; | |
| 12 | 15 | use FluentCart\App\Services\Payments\PaymentReceipt; |
| 13 | 16 | use FluentCart\App\Services\TemplateService; |
| 17 | +use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper; | |
| 14 | 18 | use FluentCart\Framework\Support\Arr; |
| 15 | 19 | use FluentCart\Framework\Support\Str; |
| 16 | 20 | use FluentCartPro\App\Modules\Licensing\Models\License; |
| 17 | 21 | |
| @@ -50,10 +54,25 @@ | ||
| 50 | 54 | // 'payment_receipt' => 'getPaymentReceipt', |
| 51 | 55 | // ]; |
| 52 | 56 | |
| 53 | 57 | protected array $methodMap = [ |
| 54 | - 'item_count' => 'getItemCount', | |
| 55 | - 'is_digital' => 'getIsDigital', | |
| 58 | + 'item_count' => 'getItemCount', | |
| 59 | + 'is_digital' => 'getIsDigital', | |
| 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', | |
| 56 | 75 | ]; |
| 57 | 76 | |
| 58 | 77 | protected array $attributeMap = [ |
| 59 | 78 | 'id' => 'order.id', |
| @@ -86,14 +105,12 @@ | ||
| 86 | 105 | if (in_array($accessor, ['updated_at', 'created_at'])) { |
| 87 | 106 | $date = Arr::get($this->data, $this->attributeMap[$accessor]); |
| 88 | 107 | $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp(); |
| 89 | 108 | |
| 90 | - $date = wp_date( | |
| 91 | - get_option('date_format'), | |
| 92 | - $timestamp, | |
| 93 | - new \DateTimeZone($this->orderTz) | |
| 94 | - ); | |
| 95 | - | |
| 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 | + | |
| 96 | 113 | return Helper::translateNumber($date); |
| 97 | 114 | } |
| 98 | 115 | |
| 99 | 116 | // Handle _formatted suffix for cent columns (e.g. total_amount_formatted) |
| @@ -343,8 +360,17 @@ | ||
| 343 | 360 | |
| 344 | 361 | return is_user_logged_in() ? $orderLink : wp_login_url($orderLink); |
| 345 | 362 | } |
| 346 | 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 | + | |
| 347 | 373 | public function getAdminOrderLink($accessor, $code = null) |
| 348 | 374 | { |
| 349 | 375 | if (empty($this->order)) { |
| 350 | 376 | return $code; |
| @@ -445,12 +471,494 @@ | ||
| 445 | 471 | |
| 446 | 472 | return '0'; |
| 447 | 473 | } |
| 448 | 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 | + | |
| 449 | 790 | public function getSubscriptions() |
| 450 | 791 | { |
| 451 | 792 | return ''; |
| 452 | 793 | } |
| 453 | 794 | |
| 454 | -} | |
| 795 | + /** | |
| 796 | + * Returns formatted store VAT display string, e.g. "VAT: NL123456789B01". | |
| 797 | + * Returns empty string if no store VAT is configured for this order. | |
| 798 | + */ | |
| 799 | + public function getStoreVatDisplay(): string | |
| 800 | + { | |
| 801 | + if (!$this->order) { | |
| 802 | + return ''; | |
| 803 | + } | |
| 455 | 804 | |
| 805 | + $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null; | |
| 456 | 806 | |
| 807 | + if (!$orderTaxRate) { | |
| 808 | + return ''; | |
| 809 | + } | |
| 810 | + | |
| 811 | + $storeVatNumber = Arr::get($orderTaxRate->meta ?? [], 'store_vat_number', ''); | |
| 812 | + | |
| 813 | + if (empty($storeVatNumber)) { | |
| 814 | + return ''; | |
| 815 | + } | |
| 816 | + | |
| 817 | + $taxCountry = Arr::get($orderTaxRate->meta ?? [], 'tax_country', ''); | |
| 818 | + $label = \FluentCart\App\Modules\Tax\TaxModule::getCountryTaxTitle($taxCountry); | |
| 819 | + | |
| 820 | + return esc_html($label) . ': ' . esc_html($storeVatNumber); | |
| 821 | + } | |
| 822 | + | |
| 823 | + /** | |
| 824 | + * Returns formatted buyer VAT display string, e.g. "VAT/Tax ID: XX123456". | |
| 825 | + * Checks business_info first, then falls back to legacy VAT storage. | |
| 826 | + */ | |
| 827 | + public function getBuyerVatDisplay(): string | |
| 828 | + { | |
| 829 | + if (!$this->order) { | |
| 830 | + return ''; | |
| 831 | + } | |
| 832 | + | |
| 833 | + $vatNumber = $this->order->getCustomerTaxNumber(); | |
| 834 | + if (!empty($vatNumber)) { | |
| 835 | + $label = __('VAT/Tax ID', 'fluent-cart'); | |
| 836 | + return esc_html($label) . ': ' . esc_html($vatNumber); | |
| 837 | + } | |
| 838 | + | |
| 839 | + return ''; | |
| 840 | + } | |
| 841 | + | |
| 842 | + /** | |
| 843 | + * Returns buyer company name from billing address meta or VAT reverse charge data. | |
| 844 | + */ | |
| 845 | + public function getBuyerCompanyName(): string | |
| 846 | + { | |
| 847 | + if (!$this->order) { | |
| 848 | + return ''; | |
| 849 | + } | |
| 850 | + | |
| 851 | + // Check billing address meta first | |
| 852 | + if ($this->order->billing_address) { | |
| 853 | + $companyName = Arr::get($this->order->billing_address->meta ?? [], 'other_data.company_name', ''); | |
| 854 | + if (!empty($companyName)) { | |
| 855 | + return esc_html($companyName); | |
| 856 | + } | |
| 857 | + } | |
| 858 | + | |
| 859 | + return esc_html($this->order->getCustomerTaxName()); | |
| 860 | + } | |
| 861 | + | |
| 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', '')); | |
| 876 | + } | |
| 877 | + | |
| 878 | + public function getBuyerReverseChargeDeclaration(): string | |
| 879 | + { | |
| 880 | + if (!$this->order) { | |
| 881 | + return ''; | |
| 882 | + } | |
| 883 | + | |
| 884 | + return esc_html(Arr::get($this->order->getBusinessInfo(), 'reverse_charge_declaration', '')); | |
| 885 | + } | |
| 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 | +} | |