Skip to content

Commit

Permalink
Merge pull request #6784 from reactioncommerce/feat/create-migration-…
Browse files Browse the repository at this point in the history
…for-old-discount

feat: create migration for old discount
  • Loading branch information
brent-hoover authored Feb 13, 2023
2 parents 9db5e13 + f2a2321 commit 93381c5
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 3 deletions.
2 changes: 2 additions & 0 deletions packages/api-plugin-promotions-coupons/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import register from "./src/index.js";

export { default as migrations } from "./migrations/index.js";

export default register;
213 changes: 213 additions & 0 deletions packages/api-plugin-promotions-coupons/migrations/2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/* eslint-disable no-await-in-loop */
import Random from "@reactioncommerce/random";
import getCurrentShopTime from "./getCurrentShopTime.js";

/**
* @summary returns an auto-incrementing integer id for a specific entity
* @param {Object} db - The db instance
* @param {String} shopId - The shop ID
* @param {String} entity - The entity (normally a collection) that you are tracking the ID for
* @return {Promise<Number>} - The auto-incrementing ID to use
*/
async function incrementSequence(db, shopId, entity) {
const { value: { value } } = await db.collection("Sequences").findOneAndUpdate(
{ shopId, entity },
{ $inc: { value: 1 } },
{ returnDocument: "after" }
);
return value;
}

/**
* @summary Migration current discounts v2 to version 2
* @param {Object} db MongoDB `Db` instance
* @return {undefined}
*/
async function migrationDiscounts(db) {
const discounts = await db.collection("Discounts").find({}, { _id: 1 }).toArray();

// eslint-disable-next-line require-jsdoc
function getDiscountCalculationType(discount) {
if (discount.calculation.method === "discount") return "percentage";
if (discount.calculation.method === "shipping") return "shipping";
if (discount.calculation.method === "sale") return "flat";
return "fixed";
}

for (const { _id } of discounts) {
const discount = await db.collection("Discounts").findOne({ _id });
const promotionId = Random.id();

const now = new Date();
const shopTime = await getCurrentShopTime(db);

// eslint-disable-next-line no-await-in-loop
await db.collection("Promotions").insertOne({
_id: promotionId,
shopId: discount.shopId,
name: discount.code,
label: discount.code,
description: discount.code,
promotionType: "order-discount",
actions: [
{
actionKey: "discounts",
actionParameters: {
discountType: discount.discountType === "sale" ? "order" : "item",
discountCalculationType: getDiscountCalculationType(discount),
discountValue: Number(discount.discount)
}
}
],
triggers: [
{
triggerKey: "coupons",
triggerParameters: {
conditions: {
all: [
{
fact: "totalItemAmount",
operator: "greaterThanInclusive",
value: 0
}
]
}
}
}
],
enabled: discount.conditions.enabled,
stackability: {
key: "all",
parameters: {}
},
triggerType: "explicit",
state: "active",
startDate: shopTime[discount.shopId],
createdAt: now,
updatedAt: now,
referenceId: await incrementSequence(db, discount.shopId, "Promotions")
});

const couponId = Random.id();
await db.collection("Coupons").insertOne({
_id: couponId,
shopId: discount.shopId,
promotionId,
name: discount.code,
code: discount.code,
canUseInStore: false,
usedCount: 0,
expirationDate: null,
createdAt: now,
updatedAt: now,
maxUsageTimesPerUser: discount.conditions.accountLimit,
maxUsageTimes: discount.conditions.redemptionLimit,
discountId: discount._id
});
}
}

/**
* @summary Migration current discount to promotion and coupon
* @param {Object} db - The db instance
* @param {String} discountId - The discount ID
* @returns {Object} - The promotion
*/
async function getPromotionByDiscountId(db, discountId) {
const coupon = await db.collection("Coupons").findOne({ discountId });
if (!coupon) return null;
const promotion = await db.collection("Promotions").findOne({ _id: coupon.promotionId });
if (!promotion) return null;

promotion.relatedCoupon = {
couponId: coupon._id,
couponCode: coupon.code
};

return promotion;
}

/**
* @summary Migration current cart v1 to version 2
* @param {Object} db - The db instance
* @returns {undefined}
*/
async function migrateCart(db) {
const carts = await db.collection("Cart").find({}, { _id: 1 }).toArray();

for (const { _id } of carts) {
const cart = await db.collection("Cart").findOne({ _id });
if (cart.version && cart.version === 2) continue;

if (!cart.billing) continue;

if (!cart.appliedPromotions) cart.appliedPromotions = [];

for (const billing of cart.billing) {
if (!billing.data || !billing.data.discountId) continue;
const promotion = await getPromotionByDiscountId(db, billing.data.discountId);
cart.appliedPromotions.push(promotion);
}

cart.version = 2;
await db.collection("Cart").updateOne({ _id: cart._id }, { $set: cart });
}
}

/**
* @summary Performs migration up from previous data version
* @param {Object} context Migration context
* @param {Object} context.db MongoDB `Db` instance
* @param {Function} context.progress A function to report progress, takes percent
* number as argument.
* @return {undefined}
*/
async function up({ db, progress }) {
try {
await migrationDiscounts(db);
} catch (err) {
throw new Error(`Failed to migrate discounts: ${err.message}`);
}

progress(50);

try {
await migrateCart(db);
} catch (err) {
throw new Error(`Failed to migrate cart: ${err.message}`);
}
progress(100);
}

/**
* @summary Performs migration down from previous data version
* @param {Object} context Migration context
* @param {Object} context.db MongoDB `Db` instance
* @param {Function} context.progress A function to report progress, takes percent
* number as argument.
* @return {undefined}
*/
async function down({ db, progress }) {
const coupons = await db.collection("Coupons").find(
{ discountId: { $exists: true } },
{ _id: 1, promotionId: 1 }
).toArray();

const couponIds = coupons.map((coupon) => coupon._id);
await db.collection("Coupons").remove({ _id: { $in: couponIds } });

const promotionIds = coupons.map((coupon) => coupon.promotionId);
await db.collection("Promotions").remove({ _id: { $in: promotionIds } });

const carts = await db.collection("Cart").find({ version: 2 }, { _id: 1 }).toArray();
for (const { _id } of carts) {
const cart = await db.collection("Cart").findOne({ _id });
cart.appliedPromotions.length = 0;
cart.version = 1;
await db.collection("Cart").updateOne({ _id: cart._id }, { $set: cart });
}

progress(100);
}

export default { down, up };
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @summary if no data in cache, repopulate
* @param {Object} db - The db instance
* @return {Promise<{Object}>} - The shop timezone object after pushing data to cache
*/
async function populateCache(db) {
const Shops = db.collection("Shops");
const shopTzObject = {};
const shops = await Shops.find({}).toArray();
for (const shop of shops) {
const { _id: shopId } = shop;
shopTzObject[shopId] = shop.timezone;
}
return shopTzObject;
}

/**
* @summary get the current time in the shops timezone
* @param {Object} db - The db instance
* @return {Promise<{Object}>} - Object of shops and their current time in their timezone
*/
export default async function getCurrentShopTime(db) {
const shopTzData = await populateCache(db);
const shopNow = {};
for (const shop of Object.keys(shopTzData)) {
const now = new Date().toLocaleString("en-US", { timeZone: shopTzData[shop] });
const nowDate = new Date(now);
shopNow[shop] = nowDate;
}
return shopNow;
}
13 changes: 13 additions & 0 deletions packages/api-plugin-promotions-coupons/migrations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { migrationsNamespace } from "./migrationsNamespace.js";
import migration2 from "./2.js";

export default {
tracks: [
{
namespace: migrationsNamespace,
migrations: {
2: migration2
}
}
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const migrationsNamespace = "promotion-coupons";
2 changes: 1 addition & 1 deletion packages/api-plugin-promotions-coupons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"sideEffects": false,
"dependencies": {
"@reactioncommerce/api-utils": "^1.16.9",
"@reactioncommerce/db-version-check": "workspace:^1.0.0",
"@reactioncommerce/logger": "^1.1.3",
"@reactioncommerce/random": "^1.0.2",
"@reactioncommerce/reaction-error": "^1.0.1",
Expand All @@ -34,7 +35,6 @@
"lodash": "^4.17.21",
"simpl-schema": "^1.12.2"
},
"devDependencies": {},
"scripts": {
"lint": "npm run lint:eslint",
"lint:eslint": "eslint .",
Expand Down
23 changes: 23 additions & 0 deletions packages/api-plugin-promotions-coupons/src/preStartup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import _ from "lodash";
import SimpleSchema from "simpl-schema";
import doesDatabaseVersionMatch from "@reactioncommerce/db-version-check";
import { migrationsNamespace } from "../migrations/migrationsNamespace.js";
import { CouponTriggerCondition, CouponTriggerParameters } from "./simpleSchemas.js";

const expectedVersion = 2;

/**
* @summary This is a preStartup function that is called before the app starts up.
* @param {Object} context - The application context
Expand Down Expand Up @@ -39,4 +43,23 @@ export default async function preStartupPromotionCoupon(context) {
Cart.extend({
"appliedPromotions.$": copiedPromotion
});

const setToExpectedIfMissing = async () => {
const anyDiscount = await context.collections.Discounts.findOne();
return !anyDiscount;
};
const ok = await doesDatabaseVersionMatch({
// `db` is a Db instance from the `mongodb` NPM package,
// such as what is returned when you do `client.db()`
db: context.app.db,
// These must match one of the namespaces and versions
// your package exports in the `migrations` named export
expectedVersion,
namespace: migrationsNamespace,
setToExpectedIfMissing
});

if (!ok) {
throw new Error(`Database needs migrating. The "${migrationsNamespace}" namespace must be at version ${expectedVersion}. See docs for more information on migrations: https://github.com/reactioncommerce/api-migrations`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type Coupon {

"Coupon updated time"
updatedAt: Date!

"Related discount ID"
discountId: ID
}

extend type Promotion {
Expand Down
4 changes: 4 additions & 0 deletions packages/api-plugin-promotions-coupons/src/simpleSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export const Coupon = new SimpleSchema({
},
updatedAt: {
type: Date
},
discountId: {
type: String,
optional: true
}
});

Expand Down
4 changes: 4 additions & 0 deletions packages/api-plugin-promotions/src/preStartup.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ function extendCartSchema(context) {
const { simpleSchemas: { Cart, Promotion } } = context; // we get this here rather then importing it to get the extended version

Cart.extend({
"version": {
type: Number,
optional: true
},
"appliedPromotions": {
type: Array,
optional: true
Expand Down
5 changes: 3 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 93381c5

Please sign in to comment.