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 +202 -28 1.5.1 → 1.7.0 View file →
@@ -9,8 +9,9 @@
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;
13 14 use FluentCart\App\Services\Payments\PaymentHelper;
14 15 use FluentCart\App\Services\Payments\PaymentReceipt;
15 16 use FluentCart\App\Services\TemplateService;
16 17 use FluentCart\App\Services\Renderer\Receipt\TaxSummaryHelper;
@@ -54,8 +55,9 @@
54 55 // ];
55 56
56 57 protected array $methodMap = [
57 58 'item_count' => 'getItemCount',
59 + 'review_url' => 'getReviewUrl',
58 60 'is_digital' => 'getIsDigital',
59 61 'store_vat_display' => 'getStoreVatDisplay',
60 62 'store_company_name' => 'getStoreCompanyName',
61 63 'store_company_display' => 'getStoreCompanyDisplay',
@@ -104,14 +106,12 @@
104 106 if (in_array($accessor, ['updated_at', 'created_at'])) {
105 107 $date = Arr::get($this->data, $this->attributeMap[$accessor]);
106 108 $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp();
107 109
108 - $date = wp_date(
109 - get_option('date_format'),
110 - $timestamp,
111 - new \DateTimeZone($this->orderTz)
112 - );
113 -
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 +
114 114 return Helper::translateNumber($date);
115 115 }
116 116
117 117 // Handle _formatted suffix for cent columns (e.g. total_amount_formatted)
@@ -361,8 +361,38 @@
361 361
362 362 return is_user_logged_in() ? $orderLink : wp_login_url($orderLink);
363 363 }
364 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 +
365 395 public function getPaymentLink($accessor = null, $code = null)
366 396 {
367 397 if (empty($this->order)) {
368 398 return $code;
@@ -547,29 +577,130 @@
547 577 $normalStyle = 'padding:3px 0;font-size:11px;font-weight:600;color:#525866;';
548 578 $totalStyle = 'padding:6px 0 3px;font-size:11px;font-weight:700;color:#0E121B;border-top:1px solid #e2e8f0;';
549 579 $valueStyle = 'text-align:right;';
550 580
551 - $rows = '<tr><td colspan="2" style="' . $headingStyle . '">'
552 - . esc_html__('TAX SUMMARY', 'fluent-cart')
553 - . '</td></tr>';
581 + $rows = '';
554 582
555 - if ($summary['isReverseCharge']) {
556 - $rcReversedTotal = isset($summary['reversedTaxTotal']) ? (int) $summary['reversedTaxTotal'] : 0;
557 - $rcReversedValue = $rcReversedTotal > 0
558 - ? esc_html(Helper::toDecimal($rcReversedTotal))
559 - : 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'])) {
560 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>'
561 682 . '<td style="' . $totalStyle . '">' . esc_html__('Tax reversed', 'fluent-cart') . '</td>'
562 683 . '<td style="' . $totalStyle . $valueStyle . '">' . $rcReversedValue . '</td>'
563 684 . '</tr>';
564 - return $rows;
685 + return $this->wrapTaxBreakdownBox($rows);
565 686 }
566 687
567 - $opFeeRows = Arr::get($summary, 'feeTaxLineRows', []);
568 - $rowCount = (int) ($summary['inclusiveTax'] > 0) + (int) ($summary['exclusiveTax'] > 0) + count($opFeeRows) + (int) ($summary['shippingTax'] > 0);
569 - $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>';
570 691
571 - 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) {
572 703 $rows .= '<tr>'
573 704 . '<td style="' . $mutedStyle . '">' . esc_html__('Included in item prices', 'fluent-cart') . '</td>'
574 705 . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['inclusiveTax'], $currency) . '</td>'
575 706 . '</tr>';
@@ -574,9 +705,9 @@
574 705 . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['inclusiveTax'], $currency) . '</td>'
575 706 . '</tr>';
576 707 }
577 708
578 - if ($summary['exclusiveTax'] > 0 && $shouldShowBreakdown) {
709 + if (empty($taxRateLines) && $summary['exclusiveTax'] > 0 && $shouldShowBreakdown) {
579 710 $rows .= '<tr>'
580 711 . '<td style="' . $normalStyle . '">' . esc_html__('Added on products', 'fluent-cart') . '</td>'
581 712 . '<td style="' . $normalStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['exclusiveTax'], $currency) . '</td>'
582 713 . '</tr>';
@@ -594,15 +725,24 @@
594 725
595 726 if ($summary['shippingTax'] > 0 && $shouldShowBreakdown) {
596 727 $isShippingInclusive = TaxSummaryHelper::isShippingTaxInclusive($this->order);
597 728 $shippingRowStyle = $isShippingInclusive ? $mutedStyle : $normalStyle;
598 - $shippingRowLabel = $isShippingInclusive
599 - ? esc_html__('Included in shipping prices', 'fluent-cart')
600 - : esc_html__('Added on shipping', 'fluent-cart');
601 - $rows .= '<tr>'
602 - . '<td style="' . $shippingRowStyle . '">' . $shippingRowLabel . '</td>'
603 - . '<td style="' . $shippingRowStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['shippingTax'], $currency) . '</td>'
604 - . '</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 + }
605 745 }
606 746
607 747 if ($summary['payableTax'] > 0) {
608 748 $rows .= '<tr>'
@@ -617,9 +757,43 @@
617 757 . '<td style="' . $mutedStyle . $valueStyle . '">' . CurrencySettings::getPriceHtml($summary['totalOrderTax'], $currency) . '</td>'
618 758 . '</tr>';
619 759 }
620 760
621 - 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>';
622 796 }
623 797
624 798 private function resolveTaxRateLabel($orderTaxRate): string
625 799 {