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 +212 -28 1.4.2 → 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 {