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 +127 -1 1.5.5 → 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',
@@ -81,9 +175,9 @@
81 175 {
82 176 $customer = $this->getDummyCustomer();
83 177 $transaction = $this->getDummyTransaction();
84 178
85 - return new class($customer, $transaction, new Collection()) {
179 + $order = new class($customer, $transaction, new Collection()) {
86 180 public $id = 1001;
87 181 public $uuid = 'preview-order-uuid';
88 182 public $invoice_no = 'FC-1001';
89 183 public $customer;
@@ -103,8 +197,9 @@
103 197 public $total_amount = 4900;
104 198 public $total_paid = 0;
105 199 public $total_refund = 0;
106 200 public $payment_status = 'pending';
201 + public $config = [];
107 202 private $latestTransaction;
108 203
109 204 public function __construct($customer, $transaction, $emptyCollection)
110 205 {
@@ -144,8 +239,26 @@
144 239 {
145 240 return '#';
146 241 }
147 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 +
148 261 public function getLatestTransaction()
149 262 {
150 263 return $this->latestTransaction;
151 264 }
@@ -159,8 +272,14 @@
159 272 {
160 273 return new Collection([]);
161 274 }
162 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;
163 282 }
164 283
165 284 private function getDummySubscription($order): object
166 285 {
@@ -170,8 +289,9 @@
170 289 return new class($customer, $transaction, $order) {
171 290 public $id = 2001;
172 291 public $uuid = 'preview-subscription-uuid';
173 292 public $item_name = 'Sample Subscription';
293 + public $display_item_name = 'Sample Subscription';
174 294 public $billing_interval = 'yearly';
175 295 public $next_billing_date;
176 296 public $recurring_total = 4900;
177 297 public $status = 'active';
@@ -184,8 +304,9 @@
184 304 {
185 305 $this->customer = $customer;
186 306 $this->latestTransaction = $transaction;
187 307 $this->order = $order;
308 + $transaction->order = $order;
188 309 $this->next_billing_date = \FluentCart\App\Services\DateTime\DateTime::gmtNow()->addDays(30)->format('Y-m-d H:i:s');
189 310 $this->trial_ends_at = \FluentCart\App\Services\DateTime\DateTime::gmtNow()->addDays(3)->format('Y-m-d H:i:s');
190 311 }
191 312
@@ -201,8 +322,13 @@
201 322
202 323 public function getReactivateUrl()
203 324 {
204 325 return '#';
326 + }
327 +
328 + public function getPaymentMethodText()
329 + {
330 + return 'Visa ***4242';
205 331 }
206 332 };
207 333 }
208 334 }