From 03e63e414b60e813c18c9815f6d8d3f01efdcf11 Mon Sep 17 00:00:00 2001 From: andela-aatanda Date: Tue, 28 Mar 2017 22:40:41 +0100 Subject: [PATCH] Cart Cleanup Job (#2013) * add cart-cleanup implementation * import cart cleanup job and call its function * update cart cleanup job to remove stale cart for anonymous user * update registry schema to hold cart cleanup schedule and reminder * add cart cleanup schedule and email reminder fields to view * update reminder and cleanup field in shops settings to hold default values * update select dropdown values and placeholder in view * update cart cleanup job for users with account * remove unnecessary field * remove unnecessary field and fix indent issue * refactor cart cleanup job * remove dropdown and style fix * proper naming field * update job to cleanup anonymous user cart and account * export session collection * update cart cleanup job to clear anonymous user's session * add cart cleanupDurationDays to reaction.json.example * update job to run after settings has been loaded from reaction.json * correct misleading placeholder * refactor job to purge anonymous user stale carts/account/sessions * add JSDoc for purge function * add function that fetches stale carts * implement review --- .../templates/shop/settings/settings.html | 1 + .../included/jobcontrol/server/index.js | 3 + .../included/jobcontrol/server/jobs/cart.js | 87 +++++++++++++++++++ lib/collections/schemas/registry.js | 5 ++ private/settings/reaction.json.example | 3 + server/publications/collections/sessions.js | 3 +- 6 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 imports/plugins/included/jobcontrol/server/jobs/cart.js diff --git a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html index 59125911efc..27fa2420624 100644 --- a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html +++ b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html @@ -102,6 +102,7 @@ {{> afQuickField name='settings.openexchangerates.refreshPeriod' placeholder="every 1 hour"}} {{> afQuickField name='settings.google.clientId'}} {{> afQuickField name='settings.google.apiKey'}} + {{> afQuickField name='settings.cart.cleanupDurationDays' placeholder="older than 3 days"}} {{> shopSettingsSubmitButton}} {{/autoForm}} diff --git a/imports/plugins/included/jobcontrol/server/index.js b/imports/plugins/included/jobcontrol/server/index.js index a1d7e389ce3..66c021ddf1d 100644 --- a/imports/plugins/included/jobcontrol/server/index.js +++ b/imports/plugins/included/jobcontrol/server/index.js @@ -1,8 +1,11 @@ import "./jobs/exchangerates"; import "./jobs/cleanup"; +import "./jobs/cart"; import cleanupJob from "./jobs/cleanup"; import fetchRateJobs from "./jobs/exchangerates"; +import cartCleanupJob from "./jobs/cart"; import "./i18n"; cleanupJob(); fetchRateJobs(); +cartCleanupJob(); diff --git a/imports/plugins/included/jobcontrol/server/jobs/cart.js b/imports/plugins/included/jobcontrol/server/jobs/cart.js new file mode 100644 index 00000000000..a8eadc23e16 --- /dev/null +++ b/imports/plugins/included/jobcontrol/server/jobs/cart.js @@ -0,0 +1,87 @@ +import later from "later"; +import moment from "moment"; +import { Accounts, Cart, Jobs } from "/lib/collections"; +import { Hooks, Logger, Reaction } from "/server/api"; +import { ServerSessions } from "/server/publications/collections/sessions"; + + +Hooks.Events.add("afterCoreInit", () => { + Logger.debug("Adding Job removeStaleCart and Accounts to jobControl"); + const settings = Reaction.getShopSettings(); + if (settings.cart) { + new Job(Jobs, "cart/removeFromCart", {}) + .priority("normal") + .retry({ + retries: 5, + wait: 60000, + backoff: "exponential" // delay by twice as long for each subsequent retry + }) + .repeat({ + schedule: later.parse.text("every day") + }) + .save({ + cancelRepeats: true + }); + } else { + Logger.warn("No cart cleanup schedule"); + } +}); + +/** + * {Function} that fetches stale carts + * @param {Object} olderThan older than date + * @return {Object} stale carts + */ +const getstaleCarts = (olderThan) => { + return Cart.find({ updatedAt: { $lte: olderThan } }).fetch(); +}; + +export default () => { + const removeStaleCart = Jobs.processJobs("cart/removeFromCart", { + pollInterval: 60 * 60 * 1000, // backup polling, see observer below + workTimeout: 180 * 1000 + }, (job, callback) => { + Logger.debug("Processing cart/removeFromCart"); + const settings = Reaction.getShopSettings(); + if (settings.cart) { + const schedule = (settings.cart.cleanupDurationDays).match(/\d/);// configurable in shop settings + const olderThan = moment().subtract(Number(schedule[0]), "days")._d; + const carts = getstaleCarts(olderThan); + carts.forEach(cart => { + const user = Accounts.findOne({ _id: cart.userId }); + if (!user.emails.length) { + const removeCart = Cart.remove({ userId: user._id }); + const removeAccount = Accounts.remove( + { + _id: cart.userId, + emails: [] + } + ); + const destroySession = ServerSessions.remove({ _id: cart.sessionId }); + Meteor.users.remove({ _id: user._id, emails: [] }); // clears out anonymous user + if (removeCart && removeAccount && destroySession) { + const success = "Stale anonymous user cart and account successfully cleaned"; + Logger.debug(success); + job.done(success, { repeatId: true }); + } + } else { + Cart.remove({ userId: user._id }); + const success = "Stale user cart successfully cleaned"; + Logger.debug(success); + job.done(success, { repeatId: true }); + } + }); + } else { + Logger.warn("No cart cleanup schedule"); + } + callback(); + }); + Jobs.find({ + type: "cart/removeFromCart", + status: "ready" + }).observe({ + added() { + return removeStaleCart.trigger(); + } + }); +}; diff --git a/lib/collections/schemas/registry.js b/lib/collections/schemas/registry.js index ec491d0d997..717d7290002 100644 --- a/lib/collections/schemas/registry.js +++ b/lib/collections/schemas/registry.js @@ -196,6 +196,11 @@ export const CorePackageConfig = new SimpleSchema([ "settings.public.allowGuestCheckout": { type: Boolean, label: "Allow Guest Checkout" + }, + "settings.cart.cleanupDurationDays": { + type: String, + label: "Cleanup Schedule", + defaultValue: "older than 3 days" } } ]); diff --git a/private/settings/reaction.json.example b/private/settings/reaction.json.example index c5cde426b28..44d822a5059 100644 --- a/private/settings/reaction.json.example +++ b/private/settings/reaction.json.example @@ -20,6 +20,9 @@ "host": "", "port": "" }, + "cart": { + "cleanupDurationDays": "older than 3 days" + }, "services": [{ "facebook": { "appId": "", diff --git a/server/publications/collections/sessions.js b/server/publications/collections/sessions.js index 4a8a13a34cf..608d0abed44 100644 --- a/server/publications/collections/sessions.js +++ b/server/publications/collections/sessions.js @@ -8,7 +8,8 @@ import { Reaction } from "/server/api"; * If no session is loaded, creates a new one */ -this.ServerSessions = new Mongo.Collection("Sessions"); +export const ServerSessions = new Mongo.Collection("Sessions"); +this.ServerSessions = ServerSessions; Meteor.publish("Sessions", function (sessionId) { check(sessionId, Match.OneOf(String, null));