← All changes
|
app/Services/ShortCodeParser/Parsers/OrderParser.php
+212
-28
1.4.1
→
1.7.0
View file →
| @@ -9,8 +9,10 @@ | ||
| 9 | 9 | use FluentCart\App\Models\Order; |
| 10 | 10 | use FluentCart\App\Models\Subscription; |
| 11 | 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; |
| 15 | 17 | use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper; |
| 16 | 18 | use FluentCart\Framework\Support\Arr; |
| @@ -53,8 +55,9 @@ | ||
| 53 | 55 | // ]; |
| 54 | 56 | |
| 55 | 57 | protected array $methodMap = [ |
| 56 | 58 | 'item_count' => 'getItemCount', |
| 59 | + 'review_url' => 'getReviewUrl', | |
| 57 | 60 | 'is_digital' => 'getIsDigital', |
| 58 | 61 | 'store_vat_display' => 'getStoreVatDisplay', |
| 59 | 62 | 'store_company_name' => 'getStoreCompanyName', |
| 60 | 63 | 'store_company_display' => 'getStoreCompanyDisplay', |
| @@ -103,14 +106,12 @@ | ||
| 103 | 106 | if (in_array($accessor, ['updated_at', 'created_at'])) { |
| 104 | 107 | $date = Arr::get($this->data, $this->attributeMap[$accessor]); |
| 105 | 108 | $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp(); |
| 106 | 109 | |
| 107 | - $date = wp_date( | |
| 108 | - get_option('date_format'), | |
| 109 | - $timestamp, | |
| 110 | - new \DateTimeZone($this->orderTz) | |
| 111 | - ); | |
| 112 | - | |
| 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 | + | |
| 113 | 114 | return Helper::translateNumber($date); |
| 114 | 115 | } |
| 115 | 116 | |
| 116 | 117 | // Handle _formatted suffix for cent columns (e.g. total_amount_formatted) |
| @@ -360,8 +361,47 @@ | ||
| 360 | 361 | |
| 361 | 362 | return is_user_logged_in() ? $orderLink : wp_login_url($orderLink); |
| 362 | 363 | } |
| 363 | 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 | + | |
| 364 | 404 | public function getAdminOrderLink($accessor, $code = null) |
| 365 | 405 | { |
| 366 | 406 | if (empty($this->order)) { |
| 367 | 407 | return $code; |
| @@ -537,29 +577,130 @@ | ||
| 537 | 577 | $normalStyle = 'padding:3px 0;font-size:11px;font-weight:600;color:#525866;'; |
| 538 | 578 | $totalStyle = 'padding:6px 0 3px;font-size:11px;font-weight:700;color:#0E121B;border-top:1px solid #e2e8f0;'; |
| 539 | 579 | $valueStyle = 'text-align:right;'; |
| 540 | 580 | |
| 541 | - $rows = '<tr><td colspan="2" style="' . $headingStyle . '">' | |
| 542 | - . esc_html__('TAX SUMMARY', 'fluent-cart') | |
| 543 | - . '</td></tr>'; | |
| 581 | + $rows = ''; | |
| 544 | 582 | |
| 545 | - if ($summary['isReverseCharge']) { | |
| 546 | - $rcReversedTotal = isset($summary['reversedTaxTotal']) ? (int) $summary['reversedTaxTotal'] : 0; | |
| 547 | - $rcReversedValue = $rcReversedTotal > 0 | |
| 548 | - ? esc_html(Helper::toDecimal($rcReversedTotal)) | |
| 549 | - : esc_html__('Charge reversed', 'fluent-cart'); | |
| 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'])) { | |
| 550 | 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>'; | |
| 638 | + } | |
| 639 | + $nestedTable .= '</table>'; | |
| 640 | + | |
| 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); | |
| 675 | + } | |
| 676 | + | |
| 677 | + if ($isRcOrder) { | |
| 678 | + $rows .= '<tr><td colspan="2" style="' . $headingStyle . '">' | |
| 679 | + . esc_html__('TAX SUMMARY', 'fluent-cart') | |
| 680 | + . '</td></tr>'; | |
| 681 | + $rows .= '<tr>' | |
| 551 | 682 | . '<td style="' . $totalStyle . '">' . esc_html__('Tax reversed', 'fluent-cart') . '</td>' |
| 552 | 683 | . '<td style="' . $totalStyle . $valueStyle . '">' . $rcReversedValue . '</td>' |
| 553 | 684 | . '</tr>'; |
| 554 | - return $rows; | |
| 685 | + return $this->wrapTaxBreakdownBox($rows); | |
| 555 | 686 | } |
| 556 | 687 | |
| 557 | - $opFeeRows = Arr::get($summary, 'feeTaxLineRows', []); | |
| 558 | - $rowCount = (int) ($summary['inclusiveTax'] > 0) + (int) ($summary['exclusiveTax'] > 0) + count($opFeeRows) + (int) ($summary['shippingTax'] > 0); | |
| 559 | - $shouldShowBreakdown = $rowCount >= 2 || ($rowCount === 1 && !($summary['payableTax'] > 0 || $summary['inclusiveTax'] > 0 || (int) Arr::get($summary, 'inclusiveFeeTax', 0) > 0)); | |
| 688 | + $rows .= '<tr><td colspan="2" style="' . $headingStyle . '">' | |
| 689 | + . esc_html__('TAX SUMMARY', 'fluent-cart') | |
| 690 | + . '</td></tr>'; | |
| 560 | 691 | |
| 561 | - if ($summary['inclusiveTax'] > 0 && $shouldShowBreakdown) { | |
| 692 | + if (!empty($taxRateLines) && $shouldShowBreakdown) { | |
| 693 | + foreach ($taxRateLines as $taxLine) { | |
| 694 | + $taxLineStyle = !empty($taxLine['inclusive']) ? $mutedStyle : $normalStyle; | |
| 695 | + $rows .= '<tr>' | |
| 696 | + . '<td style="' . $taxLineStyle . '">' . esc_html($taxLine['label']) . '</td>' | |
| 697 | + . '<td style="' . $taxLineStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxLine['order_tax'], $currency) . '</td>' | |
| 698 | + . '</tr>'; | |
| 699 | + } | |
| 700 | + } | |
| 701 | + | |
| 702 | + if (empty($taxRateLines) && $summary['inclusiveTax'] > 0 && $shouldShowBreakdown) { | |
| 562 | 703 | $rows .= '<tr>' |
| 563 | 704 | . '<td style="' . $mutedStyle . '">' . esc_html__('Included in item prices', 'fluent-cart') . '</td>' |
| 564 | 705 | . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['inclusiveTax'], $currency) . '</td>' |
| 565 | 706 | . '</tr>'; |
| @@ -564,9 +705,9 @@ | ||
| 564 | 705 | . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['inclusiveTax'], $currency) . '</td>' |
| 565 | 706 | . '</tr>'; |
| 566 | 707 | } |
| 567 | 708 | |
| 568 | - if ($summary['exclusiveTax'] > 0 && $shouldShowBreakdown) { | |
| 709 | + if (empty($taxRateLines) && $summary['exclusiveTax'] > 0 && $shouldShowBreakdown) { | |
| 569 | 710 | $rows .= '<tr>' |
| 570 | 711 | . '<td style="' . $normalStyle . '">' . esc_html__('Added on products', 'fluent-cart') . '</td>' |
| 571 | 712 | . '<td style="' . $normalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['exclusiveTax'], $currency) . '</td>' |
| 572 | 713 | . '</tr>'; |
| @@ -584,15 +725,24 @@ | ||
| 584 | 725 | |
| 585 | 726 | if ($summary['shippingTax'] > 0 && $shouldShowBreakdown) { |
| 586 | 727 | $isShippingInclusive = TaxSummaryHelper::isShippingTaxInclusive($this->order); |
| 587 | 728 | $shippingRowStyle = $isShippingInclusive ? $mutedStyle : $normalStyle; |
| 588 | - $shippingRowLabel = $isShippingInclusive | |
| 589 | - ? esc_html__('Included in shipping prices', 'fluent-cart') | |
| 590 | - : esc_html__('Added on shipping', 'fluent-cart'); | |
| 591 | - $rows .= '<tr>' | |
| 592 | - . '<td style="' . $shippingRowStyle . '">' . $shippingRowLabel . '</td>' | |
| 593 | - . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['shippingTax'], $currency) . '</td>' | |
| 594 | - . '</tr>'; | |
| 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 | + } | |
| 736 | + } else { | |
| 737 | + $shippingRowLabel = $isShippingInclusive | |
| 738 | + ? esc_html__('Included in shipping prices', 'fluent-cart') | |
| 739 | + : esc_html__('Added on shipping', 'fluent-cart'); | |
| 740 | + $rows .= '<tr>' | |
| 741 | + . '<td style="' . $shippingRowStyle . '">' . $shippingRowLabel . '</td>' | |
| 742 | + . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['shippingTax'], $currency) . '</td>' | |
| 743 | + . '</tr>'; | |
| 744 | + } | |
| 595 | 745 | } |
| 596 | 746 | |
| 597 | 747 | if ($summary['payableTax'] > 0) { |
| 598 | 748 | $rows .= '<tr>' |
| @@ -607,9 +757,43 @@ | ||
| 607 | 757 | . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['totalOrderTax'], $currency) . '</td>' |
| 608 | 758 | . '</tr>'; |
| 609 | 759 | } |
| 610 | 760 | |
| 611 | - return $rows; | |
| 761 | + return $this->wrapTaxBreakdownBox($rows); | |
| 762 | + } | |
| 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>'; | |
| 612 | 796 | } |
| 613 | 797 | |
| 614 | 798 | private function resolveTaxRateLabel($orderTaxRate): string |
| 615 | 799 | { |