UNPKG

medusa-plugin-abandoned-cart

Version:
280 lines (279 loc) 11.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const medusa_1 = require("@medusajs/medusa"); const awilix_1 = require("awilix"); const medusa_core_utils_1 = require("medusa-core-utils"); const parse_duration_1 = __importDefault(require("parse-duration")); class AbandonedCartService extends medusa_1.TransactionBaseService { constructor(container, options) { super(container); this.checkTypeOfOptions = (options) => { return options.intervals !== undefined; }; this.queryBuilder = (type, take, skip, dateLimit, fromAdmin) => { const cartRepo = this.activeManager_.withRepository(this.cartRepository); return cartRepo .createQueryBuilder("cart") .leftJoinAndSelect("cart.items", "items") .leftJoinAndSelect("cart.region", "region") .leftJoinAndSelect("cart.shipping_address", "shipping_address") .where("cart.email IS NOT NULL") .andWhere("cart.email NOT LIKE :emailPattern", { emailPattern: "%storebotmail%", }) .andWhere("cart.deleted_at IS NULL") .andWhere("cart.completed_at IS NULL") .andWhere(!fromAdmin ? `cart.created_at > now() - interval '1 day' * ${dateLimit || 7}` : "cart.created_at IS NOT NULL") .andWhere(!fromAdmin ? "cart.abandoned_completed_at IS NULL" : "cart.email IS NOT NULL") .andWhere("items.id IS NOT NULL") // Ensure there are items related to the cart .orderBy("cart.created_at", "DESC") .select([ "cart.id", "cart.email", "cart.created_at", "cart.updated_at", "cart.region", "cart.context", "items", "region", "shipping_address", "cart.abandoned_count", "cart.abandoned_lastdate", "cart.abandoned_last_interval", ]) .take(take) .skip(skip)[type](); }; this.transformCart = (cart) => { if (Array.isArray(cart)) { return cart.map((c) => this.transformCart(c)); } return { id: cart.id, email: cart.email, items: cart.items.map((item) => { return { ...item, price: `${this.humanPrice_(item.unit_price, cart.region.currency_code)} ${cart.region.currency_code}`, }; }), cart_context: cart.context, first_name: cart.shipping_address?.first_name, last_name: cart.shipping_address?.last_name, totalPrice: cart.items.reduce((acc, item) => acc + item.unit_price * item.quantity, 0), created_at: cart.created_at, updated_at: cart.updated_at, currency: cart.region.currency_code, region: cart.region.id, country_code: cart.shipping_address?.country_code, region_name: cart.region.name, abandoned_count: cart.abandoned_count, abandoned_lastdate: cart.abandoned_lastdate, abandoned_last_interval: cart.abandoned_last_interval, }; }; this.cartRepository = container.cartRepository; try { this.sendGridService = container?.sendgridService; } catch (e) { this.sendGridService = undefined; } try { this.eventBusService = container?.eventBusService; } catch (e) { this.eventBusService = undefined; } let op = options; let sorted = []; this.checkTypeOfOptions(options) && (sorted = op.intervals.sort((a, b) => { if (typeof a.interval === "string" && typeof b.interval === "string") { return (0, parse_duration_1.default)(a.interval) - (0, parse_duration_1.default)(b.interval); } else if (typeof a.interval === "number" && typeof b.interval === "number") { return a.interval - b.interval; } })); if (this.checkTypeOfOptions(options) && op.intervals && sorted.length > 0) { op = { ...op, intervals: sorted.map((i) => { return { ...i, interval: typeof i.interval === "string" ? (0, parse_duration_1.default)(i.interval) : i.interval, }; }), }; } this.logger = container.logger; this.options_ = this.checkTypeOfOptions(options) ? op : options; } getCartLocale(cart) { return cart?.cart_context?.locale || "en"; } async sendAbandonedCartEmail(id, interval) { if (!this.options_.sendgridEnabled || !this.sendGridService) { this.logger.info("SendGrid is not enabled, emitting event"); await this.eventBusService.emit("cart.send-abandoned-email", { id, interval, }); return { success: true, message: "Event emitted, but SendGrid is not enabled.", }; } try { const cartRepo = this.activeManager_.withRepository(this.cartRepository); const notNullCartsPromise = await cartRepo.findOne({ where: { id, }, order: { created_at: "DESC", }, select: [ "id", "email", "created_at", "updated_at", "region", "context", "abandoned_count", ], relations: ["items", "region", "shipping_address"], }); let templateId = this.options_?.templateId; let subject = this.options_?.subject; let header = this.options_?.header; if (!notNullCartsPromise) { throw new medusa_core_utils_1.MedusaError("Not Found", "Cart not found"); } const cart = this.transformCart(notNullCartsPromise); const locale = this.getCartLocale(cart); if (this.options_.localization && (!this.checkTypeOfOptions(this.options_) || interval === undefined)) { const localeOptions = this.options_.localization[locale]; if (localeOptions) { templateId = localeOptions.templateId; subject = localeOptions.subject ?? subject; header = localeOptions.header ?? header; } } else if (this.checkTypeOfOptions(this.options_) && interval !== undefined) { const intervalOptions = this.options_.intervals.find((i) => i.interval === interval); if (intervalOptions) { templateId = intervalOptions.templateId; subject = intervalOptions.subject ?? subject; header = intervalOptions.header ?? header; if (intervalOptions.localization) { const localeOptions = intervalOptions.localization[locale]; if (localeOptions) { templateId = localeOptions.templateId; subject = localeOptions.subject ?? subject; header = localeOptions.header ?? header; } } } } if (!this.options_.from) { throw new medusa_core_utils_1.MedusaError("Invalid", "From is required"); } if (!templateId) { throw new medusa_core_utils_1.MedusaError("Invalid", "TemplateId is required"); } const emailData = { to: cart.email, from: this.options_.from, templateId: templateId, dynamic_template_data: { ...cart, subject: subject ?? "You left something in your cart", header: header ?? "Still thinking about it?", }, }; const emailPromise = this.sendGridService.sendEmail(emailData); // const emailPromise = Promise.resolve({}); const cartPromise = cartRepo.update(cart.id, { abandoned_lastdate: new Date().toISOString(), abandoned_count: (notNullCartsPromise?.abandoned_count || 0) + 1, abandoned_last_interval: interval || undefined, abandoned_completed_at: this.checkTypeOfOptions(this.options_) && this.options_.intervals[this.options_.intervals.length - 1] .interval === interval ? new Date().toISOString() : undefined, }); const eventPromise = this.eventBusService.emit("cart.send-abandoned-email", { id, interval, }); this.logger.info(`Sending email for cart ${id}`); await Promise.all([emailPromise, cartPromise, eventPromise]); return { success: true, message: "Email sent", }; } catch (error) { this.logger.error("Error sending abandoned cart email", { error, }); return { success: false, message: "Error sending email", }; } } humanPrice_(amount, currency) { if (!amount) { return "0.00"; } const normalized = (0, medusa_core_utils_1.humanizeAmount)(amount, currency); return normalized.toFixed(medusa_core_utils_1.zeroDecimalCurrencies.includes(currency.toLowerCase()) ? 0 : 2); } async setCartsAsCompleted(cartsIds) { if (cartsIds.length === 0) { return; } const cartRepo = this.activeManager_.withRepository(this.cartRepository); this.logger.info(`Completing ${cartsIds.length} abandoned carts`); await cartRepo.update(cartsIds, { abandoned_completed_at: new Date().toISOString(), }); } async retrieveAbandonedCarts(take = 200, skip = 0, dateLimit = 7, fromAdmin = false) { const takeNumber = +take; const skipNumber = +skip; if (isNaN(takeNumber) || isNaN(skipNumber) || takeNumber < 0 || skipNumber < 0) { throw new Error("Invalid take or skip"); } const totalCartsPromise = this.queryBuilder("getCount", takeNumber, skipNumber, +dateLimit, fromAdmin); const notNullCartsPromises = this.queryBuilder("getMany", takeNumber, skipNumber, +dateLimit, fromAdmin); const [totalCarts, carts] = await Promise.all([ totalCartsPromise, notNullCartsPromises, ]); const transformedCarts = this.transformCart(carts); return { abandoned_carts: transformedCarts, total_carts: totalCarts, }; } } AbandonedCartService.LIFE_TIME = awilix_1.Lifetime.SCOPED; exports.default = AbandonedCartService;