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/Email/EmailPreviewService.php +133 -2 1.3.23 → 1.7.0 View file →
@@ -1,8 +1,11 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Services\Email;
4 4
5 +use FluentCart\App\Helpers\Status;
6 +use FluentCart\App\Models\Product;
7 +use FluentCart\App\Models\ProductReview;
5 8 use FluentCart\App\Services\DateTime\DateTime;
6 9 use FluentCart\Framework\Support\Collection;
7 10 use FluentCart\Framework\Support\Str;
8 11
@@ -30,8 +33,34 @@
30 33 'trial_end_date' => DateTime::gmtNow()->addDays(3)->format('Y-m-d H:i:s'),
31 34 ],
32 35 ];
33 36
37 + if (Str::startsWith($template, 'order.reminder.renewal_overdue.')) {
38 + if (Str::contains($template, '.final.')) {
39 + $previewData['reminder']['stage'] = 'overdue_7';
40 + } elseif (Str::contains($template, '.followup.')) {
41 + $previewData['reminder']['stage'] = 'overdue_3';
42 + } else {
43 + $previewData['reminder']['stage'] = 'overdue_1';
44 + }
45 + }
46 +
47 + // The review templates read a review, not an order. Without one the
48 + // preview of any of them fatals on the first property it touches.
49 + if (Str::startsWith($template, 'review.')) {
50 + $previewData['product'] = $this->getPreviewProduct();
51 + $previewData['review'] = $this->getDummyReview((int) $previewData['product']->ID);
52 +
53 + if (Str::startsWith($template, 'review.replied.')) {
54 + $previewData['reply'] = $this->getDummyReply((int) $previewData['product']->ID);
55 + }
56 + // A review is not about an order, and a real send carries none —
57 + // the order header (date, number) renders only when one is there.
58 + // Left in, the preview would show a header the email never has.
59 + $previewData['order'] = null;
60 + $previewData['transaction'] = null;
61 + }
62 +
34 63 if (Str::startsWith($template, 'subscription.')) {
35 64 $previewData['transaction'] = $subscription->getLatestTransaction();
36 65
37 66 if (Str::contains($template, 'trial_end')) {
@@ -37,8 +66,12 @@
37 66 if (Str::contains($template, 'trial_end')) {
38 67 $previewData['reminder']['stage'] = 'trial_end_3';
39 68 }
40 69
70 + if (Str::contains($template, 'renewal_failed')) {
71 + $previewData['error'] = __('Your card was declined.', 'fluent-cart');
72 + }
73 +
41 74 if (Str::contains($template, 'canceled')) {
42 75 $previewData['reason'] = __('canceled on customer request', 'fluent-cart');
43 76 $previewData['subscription'] = $this->getDummySubscription($previewData['order']);
44 77 }
@@ -46,8 +79,69 @@
46 79
47 80 return $previewData;
48 81 }
49 82
83 + /**
84 + * A stand-in review with every field the review templates and the
85 + * {{review.*}} codes read. A real model, not a stdClass: the templates
86 + * call attachItemLabels() on it and the parser type-checks for one.
87 + */
88 + /**
89 + * The product a review preview is about: a real published one when the
90 + * store has any, so the preview links where the email will — the
91 + * "See your review" button needs a page to point at, and a placeholder
92 + * has none. An empty store previews without the button, as the email
93 + * would for a product since deleted.
94 + */
95 + private function getPreviewProduct(): object
96 + {
97 + $product = Product::query()
98 + ->where('post_status', 'publish')
99 + ->orderBy('ID', 'DESC')
100 + ->first();
101 +
102 + if ($product) {
103 + return $product;
104 + }
105 +
106 + return (object) [
107 + 'ID' => 0,
108 + 'post_title' => 'Sample Product',
109 + ];
110 + }
111 +
112 + private function getDummyReview(int $postId = 0): ProductReview
113 + {
114 + $review = new ProductReview([
115 + 'post_id' => $postId,
116 + 'reviewer_name' => 'John Doe',
117 + 'reviewer_email' => '[email protected]',
118 + 'title' => 'Exactly what I was looking for',
119 + 'review' => 'Arrived quickly and works just as described. Would happily buy again.',
120 + 'rating' => 5,
121 + ]);
122 + $review->id = 0;
123 + $review->status = Status::REVIEW_APPROVED;
124 +
125 + return $review;
126 + }
127 +
128 + private function getDummyReply(int $postId = 0): ProductReview
129 + {
130 + $reply = new ProductReview([
131 + 'post_id' => $postId,
132 + 'parent_id' => 0,
133 + 'reviewer_name' => 'The Store Team',
134 + 'reviewer_email' => '[email protected]',
135 + 'review' => 'Thank you so much for the kind words — we are thrilled it worked out. Enjoy!',
136 + ]);
137 + $reply->id = 0;
138 + $reply->status = Status::REVIEW_APPROVED;
139 + $reply->is_admin_reply = 1;
140 +
141 + return $reply;
142 + }
143 +
50 144 private function getDummyCustomer(): object
51 145 {
52 146 return (object) [
53 147 'full_name' => 'John Doe',
@@ -59,13 +153,18 @@
59 153
60 154 private function getDummyTransaction(): object
61 155 {
62 156 return new class {
63 - public $created_at = '';
157 + public $created_at;
64 158 public $total = 4900;
65 159 public $vendor_charge_id = 'ch_preview_123';
66 160 public $order = null;
67 161
162 + public function __construct()
163 + {
164 + $this->created_at = DateTime::gmtNow();
165 + }
166 +
68 167 public function getPaymentMethodText()
69 168 {
70 169 return 'card';
71 170 }
@@ -76,9 +175,9 @@
76 175 {
77 176 $customer = $this->getDummyCustomer();
78 177 $transaction = $this->getDummyTransaction();
79 178
80 - return new class($customer, $transaction, new Collection()) {
179 + $order = new class($customer, $transaction, new Collection()) {
81 180 public $id = 1001;
82 181 public $uuid = 'preview-order-uuid';
83 182 public $invoice_no = 'FC-1001';
84 183 public $customer;
@@ -98,8 +197,9 @@
98 197 public $total_amount = 4900;
99 198 public $total_paid = 0;
100 199 public $total_refund = 0;
101 200 public $payment_status = 'pending';
201 + public $config = [];
102 202 private $latestTransaction;
103 203
104 204 public function __construct($customer, $transaction, $emptyCollection)
105 205 {
@@ -139,8 +239,26 @@
139 239 {
140 240 return '#';
141 241 }
142 242
243 + public function getMeta($metaKey, $defaultValue = false)
244 + {
245 + if ($metaKey === 'due_date') {
246 + return gmdate('Y-m-d H:i:s', strtotime('+7 days'));
247 + }
248 + return $defaultValue;
249 + }
250 +
251 + public function isReverseChargeTaxOrder()
252 + {
253 + return false;
254 + }
255 +
256 + public function getOrderRcMode()
257 + {
258 + return 'fixed';
259 + }
260 +
143 261 public function getLatestTransaction()
144 262 {
145 263 return $this->latestTransaction;
146 264 }
@@ -154,8 +272,14 @@
154 272 {
155 273 return new Collection([]);
156 274 }
157 275 };
276 +
277 + // Templates reach the order through the transaction too (e.g. the
278 + // renewal admin email's $transaction->order->getViewUrl('admin')).
279 + $transaction->order = $order;
280 +
281 + return $order;
158 282 }
159 283
160 284 private function getDummySubscription($order): object
161 285 {
@@ -165,8 +289,9 @@
165 289 return new class($customer, $transaction, $order) {
166 290 public $id = 2001;
167 291 public $uuid = 'preview-subscription-uuid';
168 292 public $item_name = 'Sample Subscription';
293 + public $display_item_name = 'Sample Subscription';
169 294 public $billing_interval = 'yearly';
170 295 public $next_billing_date;
171 296 public $recurring_total = 4900;
172 297 public $status = 'active';
@@ -179,8 +304,9 @@
179 304 {
180 305 $this->customer = $customer;
181 306 $this->latestTransaction = $transaction;
182 307 $this->order = $order;
308 + $transaction->order = $order;
183 309 $this->next_billing_date = \FluentCart\App\Services\DateTime\DateTime::gmtNow()->addDays(30)->format('Y-m-d H:i:s');
184 310 $this->trial_ends_at = \FluentCart\App\Services\DateTime\DateTime::gmtNow()->addDays(3)->format('Y-m-d H:i:s');
185 311 }
186 312
@@ -196,8 +322,13 @@
196 322
197 323 public function getReactivateUrl()
198 324 {
199 325 return '#';
326 + }
327 +
328 + public function getPaymentMethodText()
329 + {
330 + return 'Visa ***4242';
200 331 }
201 332 };
202 333 }
203 334 }