| @@ -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 | { |
| @@ -106,14 +206,28 @@ | ||
| 106 | 206 | $this->customer = $customer; |
| 107 | 207 | $this->latestTransaction = $transaction; |
| 108 | 208 | $this->order_items = new Collection([ |
| 109 | 209 | [ |
| 110 | - 'post_title' => 'Sample Product', | |
| 111 | - 'quantity' => 1, | |
| 112 | - 'title' => 'Standard Plan', | |
| 113 | - 'payment_type' => 'onetime', | |
| 114 | - 'payment_info' => '', | |
| 115 | - 'formatted_total' => '$49.00', | |
| 210 | + 'post_title' => 'Sample Product', | |
| 211 | + 'quantity' => 1, | |
| 212 | + 'title' => 'Standard Plan', | |
| 213 | + 'payment_type' => 'onetime', | |
| 214 | + 'payment_info' => '', | |
| 215 | + 'formatted_total' => '$49.00', | |
| 216 | + 'package_info' => __('Package: Medium Box · 30 × 20 × 15 cm · Wt: 2 kg · Shipping wt: 2.5 kg', 'fluent-cart'), | |
| 217 | + 'other_info' => [ | |
| 218 | + 'package_name' => 'Medium Box', | |
| 219 | + 'package_type' => 'box', | |
| 220 | + 'package_length' => 30, | |
| 221 | + 'package_width' => 20, | |
| 222 | + 'package_height' => 15, | |
| 223 | + 'package_dimension_unit' => 'cm', | |
| 224 | + 'weight' => 2, | |
| 225 | + 'weight_unit' => 'kg', | |
| 226 | + 'package_weight' => 0.5, | |
| 227 | + 'package_weight_unit' => 'kg', | |
| 228 | + 'package_slug' => 'medium-box', | |
| 229 | + ], | |
| 116 | 230 | ], |
| 117 | 231 | ]); |
| 118 | 232 | $this->subscriptions = $emptyCollection; |
| 119 | 233 | $this->orderTaxRates = $emptyCollection; |
| @@ -125,8 +239,26 @@ | ||
| 125 | 239 | { |
| 126 | 240 | return '#'; |
| 127 | 241 | } |
| 128 | 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 | + | |
| 129 | 261 | public function getLatestTransaction() |
| 130 | 262 | { |
| 131 | 263 | return $this->latestTransaction; |
| 132 | 264 | } |
| @@ -140,8 +272,14 @@ | ||
| 140 | 272 | { |
| 141 | 273 | return new Collection([]); |
| 142 | 274 | } |
| 143 | 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; | |
| 144 | 282 | } |
| 145 | 283 | |
| 146 | 284 | private function getDummySubscription($order): object |
| 147 | 285 | { |
| @@ -151,8 +289,9 @@ | ||
| 151 | 289 | return new class($customer, $transaction, $order) { |
| 152 | 290 | public $id = 2001; |
| 153 | 291 | public $uuid = 'preview-subscription-uuid'; |
| 154 | 292 | public $item_name = 'Sample Subscription'; |
| 293 | + public $display_item_name = 'Sample Subscription'; | |
| 155 | 294 | public $billing_interval = 'yearly'; |
| 156 | 295 | public $next_billing_date; |
| 157 | 296 | public $recurring_total = 4900; |
| 158 | 297 | public $status = 'active'; |
| @@ -165,8 +304,9 @@ | ||
| 165 | 304 | { |
| 166 | 305 | $this->customer = $customer; |
| 167 | 306 | $this->latestTransaction = $transaction; |
| 168 | 307 | $this->order = $order; |
| 308 | + $transaction->order = $order; | |
| 169 | 309 | $this->next_billing_date = \FluentCart\App\Services\DateTime\DateTime::gmtNow()->addDays(30)->format('Y-m-d H:i:s'); |
| 170 | 310 | $this->trial_ends_at = \FluentCart\App\Services\DateTime\DateTime::gmtNow()->addDays(3)->format('Y-m-d H:i:s'); |
| 171 | 311 | } |
| 172 | 312 | |
| @@ -182,8 +322,13 @@ | ||
| 182 | 322 | |
| 183 | 323 | public function getReactivateUrl() |
| 184 | 324 | { |
| 185 | 325 | return '#'; |
| 326 | + } | |
| 327 | + | |
| 328 | + public function getPaymentMethodText() | |
| 329 | + { | |
| 330 | + return 'Visa ***4242'; | |
| 186 | 331 | } |
| 187 | 332 | }; |
| 188 | 333 | } |
| 189 | 334 | } |