Skip to content

Commit f568716

Browse files
committed
feat(emails): Handle abandoned carts and send recovery emails when enabled
1 parent 6fac5a0 commit f568716

File tree

5 files changed

+120
-5
lines changed

5 files changed

+120
-5
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { ApiError } from '@cloudcommerce/api';
2+
import { logger } from '@cloudcommerce/firebase/lib/config';
3+
import api from '@cloudcommerce/api';
4+
import getAppData from '@cloudcommerce/firebase/lib/helpers/get-app-data';
5+
import { sendEmail } from '@cloudcommerce/emails';
6+
import { getStore, getMailRender } from './util/emails-utils';
7+
import getMailTempl from './util/get-mail-templ';
8+
9+
const sendCartEmails = async () => {
10+
const appData = await getAppData('emails', ['data']);
11+
const mailOptions = appData.abandoned_cart || {};
12+
if (mailOptions.disable_customer === true) {
13+
return;
14+
}
15+
const daysDelay = parseInt(appData.is_abandoned_after_days, 10) || 1;
16+
const d = new Date();
17+
d.setDate(d.getDate() - daysDelay);
18+
const {
19+
data: { result: abandonedCarts },
20+
} = await api.get('carts', {
21+
params: {
22+
'completed': false,
23+
'available': true,
24+
'flags': 'open-checkout',
25+
'updated_at<': d.toISOString(),
26+
},
27+
fields: ['_id', 'flags', 'completed', 'customers'] as const,
28+
sort: ['-updated_at'],
29+
limit: 100,
30+
});
31+
if (!abandonedCarts.length) {
32+
return;
33+
}
34+
const store = getStore();
35+
const lang = appData.lang === 'Inglês' ? 'en_us' : (store.lang || 'pt_br');
36+
const mailTempl = getMailTempl('abandoned_cart')!;
37+
const subject = mailTempl.subject[lang as 'pt_br'];
38+
const customMessage = mailOptions.custom_message;
39+
const render = await getMailRender(mailTempl?.templ);
40+
for (let i = 0; i < abandonedCarts.length; i++) {
41+
const cart = abandonedCarts[i];
42+
if (cart.completed || !cart.flags?.includes('open-checkout')) {
43+
logger.warn(`Invalid cart ${cart._id}`, { cart });
44+
continue;
45+
}
46+
const customerId = cart.customers?.[0];
47+
const flags: string[] = [];
48+
if (!customerId) {
49+
logger.info(`Cart ${cart._id} skipped without customer ID`);
50+
flags.push('email-skipped');
51+
} else {
52+
try {
53+
// eslint-disable-next-line no-await-in-loop
54+
const { data: customer } = await api.get(`customers/${customerId}`);
55+
// eslint-disable-next-line no-await-in-loop
56+
const html = await render(
57+
store,
58+
customer,
59+
{
60+
...cart,
61+
permalink: `https://${store.domain}/app/#/cart/`
62+
+ '?utm_source=ecomplus&utm_medium=email&utm_campaign=abandoned_cart'
63+
+ `#/cart/${cart._id}`,
64+
},
65+
lang,
66+
customMessage,
67+
);
68+
if (html) {
69+
// eslint-disable-next-line no-await-in-loop
70+
await sendEmail(
71+
{
72+
to: [{
73+
name: customer.display_name,
74+
email: customer.main_email,
75+
}],
76+
subject,
77+
html,
78+
templateId: mailOptions.template_id,
79+
templateData: {
80+
store,
81+
customer,
82+
cart,
83+
lang,
84+
customMessage,
85+
},
86+
},
87+
);
88+
flags.push('email-sent');
89+
}
90+
} catch (err: any) {
91+
const error: ApiError = err;
92+
if (error.statusCode === 404) {
93+
logger.warn(`Customer not found by id ${customerId}`, { cart });
94+
} else {
95+
logger.error(err);
96+
continue;
97+
}
98+
}
99+
}
100+
api.patch(`carts/${cart._id}`, { flags });
101+
}
102+
};
103+
104+
export default sendCartEmails;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { logger } from '@cloudcommerce/firebase/lib/config';
2+
3+
const sendPointsEmails = async () => {
4+
logger.info('@TODO');
5+
};
6+
7+
export default sendPointsEmails;

packages/apps/emails/src/transactional-emails.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import config from '@cloudcommerce/firebase/lib/config';
44
import functions from 'firebase-functions/v1';
55
import { createAppEventsFunction } from '@cloudcommerce/firebase/lib/helpers/pubsub';
66
import handleApiEvent from './event-to-emails';
7-
// import handleAbandonedCarts from './functios-lib/abandoned-carts';
7+
import sendCartEmails from './cron-cart-emails';
8+
import sendPointsEmails from './cron-points-emails';
89

910
const { httpsFunctionOptions: { region } } = config.get();
1011

@@ -14,7 +15,12 @@ export const emails = {
1415
cronAbandonedCarts: functions.region(region).pubsub
1516
.schedule(process.env.CRONTAB_EMAILS_ABANDONED_CARTS || '25 */3 * * *')
1617
.onRun(() => {
17-
// TODO: Refactor abandoned carts handler
18-
functions.logger.info('// TODO');
18+
return sendCartEmails();
19+
}),
20+
21+
cronExpiringPoints: functions.region(region).pubsub
22+
.schedule(process.env.CRONTAB_EMAILS_EXPIRING_POINTS || '37 14 * * 1,4')
23+
.onRun(() => {
24+
return sendPointsEmails();
1925
}),
2026
};

packages/apps/loyalty-points/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"dependencies": {
3333
"@cloudcommerce/api": "workspace:*",
3434
"@cloudcommerce/firebase": "workspace:*",
35-
"@ecomplus/transactional-mails": "^2.2.1",
3635
"@ecomplus/utils": "1.5.0-rc.6"
3736
},
3837
"devDependencies": {

packages/emails/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ const sendEmail = (
120120
const sendGrid = {
121121
send: sendEmailSendGrid,
122122
setConfig: setApiKeySendGrid,
123-
124123
};
125124

126125
const smtp = {

0 commit comments

Comments
 (0)