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/EmailNotificationMailer.php +178 -0 1.6.4 → 1.7.0 View file →
@@ -4,10 +4,12 @@
4 4
5 5 use FluentCart\App\App;
6 6 use FluentCart\App\Models\Model;
7 7 use FluentCart\App\Models\Order;
8 +use FluentCart\App\Models\ProductReview;
8 9 use FluentCart\App\Models\Subscription;
9 10 use FluentCart\App\Services\OrderService;
11 +use FluentCart\App\Services\ProductReviewService;
10 12 use FluentCart\App\Services\ShortCodeParser\ShortcodeTemplateBuilder;
11 13 use FluentCart\Framework\Support\Arr;
12 14
13 15 class EmailNotificationMailer
@@ -147,8 +149,184 @@
147 149 add_action('fluent_cart/subscription_trial_end_reminder', function ($data) {
148 150 $this->mailEmailsOfEvent('subscription_trial_end_reminder', $data);
149 151 }, 999, 1);
150 152
153 + add_action('fluent_cart/review_created', function ($data) {
154 + $review = Arr::get($data, 'review');
155 + if ($review && empty($review->parent_id)) {
156 + $this->mailEmailsOfEvent('review_created', $data);
157 + }
158 + }, 999, 1);
159 +
160 + // To the reviewer, on the settled hook — the queued job has re-read
161 + // the review and it is still approved. The job fires once per
162 + // approval, and two requests can make the same approval; the author
163 + // is told once — barring a worker killed in the one UPDATE between
164 + // the transport accepting a message and the record of it, which
165 + // repeats that one message after the lease timeout: at-least-once at
166 + // that edge, once everywhere else. So the send is behind a lease on
167 + // the review's
168 + // other_info: one conditional UPDATE, handed to exactly one caller,
169 + // taken before the send and settled after it — delivered closes the
170 + // notice, anything else releases the lease so a retry can send. Taken
171 + // only when a notification is switched on and there is someone to
172 + // send to — a lease spent on either could never be told about later.
173 + add_action('fluent_cart/review_approved_done', function ($data) {
174 + $review = Arr::get($data, 'review');
175 + if (!$review instanceof ProductReview || $review->parent_id) {
176 + return;
177 + }
178 +
179 + $mailNames = EmailNotifications::activeNotificationNamesOfEvent('review_approved_done');
180 + if (!$mailNames) {
181 + return;
182 + }
183 +
184 + if (ProductReviewService::resolveNotificationRecipient($review) === '') {
185 + // Nobody to send to. Not leased, so a corrected address and
186 + // a fresh approval can still send.
187 + return;
188 + }
189 +
190 + $leaseToken = ProductReviewService::claimApprovalNotice($review);
191 + if (!$leaseToken) {
192 + return;
193 + }
194 +
195 + $this->sendUnderNoticeLease(
196 + $mailNames,
197 + $data,
198 + ProductReviewService::deliveredApprovalNotices((int) $review->id),
199 + function (array $deliveredNames) use ($review, $leaseToken) {
200 + ProductReviewService::recordApprovalNoticeDeliveries((int) $review->id, $leaseToken, $deliveredNames);
201 + },
202 + function (bool $allDelivered, array $deliveredNames) use ($review, $leaseToken) {
203 + ProductReviewService::settleApprovalNotice((int) $review->id, $leaseToken, $allDelivered, $deliveredNames);
204 + }
205 + );
206 + }, 999, 1);
207 +
208 + // To the reviewer, when the store answers them. Same shape as the
209 + // approval notice: on the settled hook, behind a once-per-reply
210 + // lease, taken only when there is a notification switched on and
211 + // someone to send to. One more reason not to send: the person who
212 + // wrote the reply is the reviewer — a moderator answering their own
213 + // review is not told about it.
214 + add_action('fluent_cart/review_replied_done', function ($data) {
215 + $reply = Arr::get($data, 'reply');
216 + $review = Arr::get($data, 'review');
217 + if (!$reply instanceof ProductReview || !$review instanceof ProductReview || $review->parent_id) {
218 + return;
219 + }
220 +
221 + $mailNames = EmailNotifications::activeNotificationNamesOfEvent('review_replied_done');
222 + if (!$mailNames) {
223 + return;
224 + }
225 +
226 + $recipient = ProductReviewService::resolveNotificationRecipient($review);
227 + if ($recipient === '') {
228 + return;
229 + }
230 +
231 + // The reply's author, compared as who they are — the account the
232 + // reply was written from, or failing that the address it
233 + // carries — never as a role: a customer who happens to be an
234 + // administrator still gets told about replies to their review.
235 + $sameAccount = (int) $reply->user_id && (int) $reply->user_id === (int) $review->user_id;
236 + $sameAddress = strcasecmp(trim((string) $reply->reviewer_email), $recipient) === 0;
237 + if ($sameAccount || $sameAddress) {
238 + return;
239 + }
240 +
241 + $leaseToken = ProductReviewService::claimReplyNotice($reply);
242 + if (!$leaseToken) {
243 + return;
244 + }
245 +
246 + $this->sendUnderNoticeLease(
247 + $mailNames,
248 + $data,
249 + ProductReviewService::deliveredReplyNotices((int) $reply->id),
250 + function (array $deliveredNames) use ($reply, $leaseToken) {
251 + ProductReviewService::recordReplyNoticeDeliveries((int) $reply->id, $leaseToken, $deliveredNames);
252 + },
253 + function (bool $allDelivered, array $deliveredNames) use ($reply, $leaseToken) {
254 + ProductReviewService::settleReplyNotice((int) $reply->id, $leaseToken, $allDelivered, $deliveredNames);
255 + }
256 + );
257 + }, 999, 1);
258 +
259 + }
260 +
261 + /**
262 + * Send a set of notifications under a once-only lease, and settle it.
263 + *
264 + * Delivery is the transport's own word. wp_mail() fires wp_mail_succeeded
265 + * once the message is accepted and wp_mail_failed when it is not — and
266 + * fires neither when something short-circuits it, a pre_wp_mail filter
267 + * returning false, say, or a mailer filter that stripped every recipient.
268 + * So a send counts as delivered only when the success action fired for
269 + * it; silence is not success. (The action is WordPress 5.9's; the plugin
270 + * requires 6.7.) Watched only for the duration of this send, so another
271 + * email elsewhere in the request is not read as ours.
272 + *
273 + * One lease covers every notification switched on for the event — an
274 + * add-on can register a second — so each is judged on its own: the ones
275 + * already delivered under an earlier, released lease are skipped; each
276 + * one that goes out is recorded at once, before the next is tried, so a
277 + * worker killed after a delivery leaves at most that one delivery
278 + * unrecorded; and the loop stops at the first failure to leave the rest
279 + * for the retry. A throw anywhere in a send still reaches the settle on
280 + * the way out, with what went out before it, and then the caller: the
281 + * queued job records the failure and a re-run can send.
282 + *
283 + * @param string[] $mailNames notifications to send, in order
284 + * @param array $data the event payload the templates read
285 + * @param string[] $alreadyDelivered names delivered under an earlier lease
286 + * @param callable $record fn(string[] $deliveredNames): records deliveries so far, lease still held
287 + * @param callable $settle fn(bool $allDelivered, string[] $deliveredNames): settles the lease
288 + */
289 + protected function sendUnderNoticeLease(array $mailNames, array $data, array $alreadyDelivered, callable $record, callable $settle): void
290 + {
291 + $deliveredNow = [];
292 + $allDelivered = false;
293 + $deliveryFailed = false;
294 + $deliverySucceeded = false;
295 +
296 + $onMailFailed = function () use (&$deliveryFailed) {
297 + $deliveryFailed = true;
298 + };
299 + $onMailSucceeded = function () use (&$deliverySucceeded) {
300 + $deliverySucceeded = true;
301 + };
302 + add_action('wp_mail_failed', $onMailFailed);
303 + add_action('wp_mail_succeeded', $onMailSucceeded);
304 +
305 + try {
306 + foreach ($mailNames as $mailName) {
307 + if (in_array($mailName, $alreadyDelivered, true)) {
308 + continue;
309 + }
310 +
311 + $deliveryFailed = false;
312 + $deliverySucceeded = false;
313 + $this->mailByEmailName($mailName, $data);
314 +
315 + if (!$deliverySucceeded || $deliveryFailed) {
316 + $deliveryFailed = true;
317 + break;
318 + }
319 +
320 + $deliveredNow[] = $mailName;
321 + $record(array_merge($alreadyDelivered, $deliveredNow));
322 + }
323 + $allDelivered = !$deliveryFailed;
324 + } finally {
325 + remove_action('wp_mail_failed', $onMailFailed);
326 + remove_action('wp_mail_succeeded', $onMailSucceeded);
327 + $settle($allDelivered, array_merge($alreadyDelivered, $deliveredNow));
328 + }
151 329 }
152 330
153 331 public function registerAsyncMails()
154 332 {