Skip to content

Commit

Permalink
feat: improve apply coupon mutation
Browse files Browse the repository at this point in the history
Signed-off-by: vanpho93 <vanpho02@gmail.com>
  • Loading branch information
vanpho93 committed Jan 5, 2023
1 parent e1e82a2 commit b698c5d
Show file tree
Hide file tree
Showing 19 changed files with 647 additions and 55 deletions.
2 changes: 2 additions & 0 deletions packages/api-plugin-orders/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import mutations from "./mutations/index.js";
import policies from "./policies.json";
import preStartup from "./preStartup.js";
import queries from "./queries/index.js";
import { registerPluginHandlerForOrder } from "./registration.js";
import resolvers from "./resolvers/index.js";
import schemas from "./schemas/index.js";
import { Order, OrderFulfillmentGroup, OrderItem, CommonOrder, SelectedFulfillmentOption } from "./simpleSchemas.js";
Expand Down Expand Up @@ -42,6 +43,7 @@ export default async function register(app) {
}
},
functionsByType: {
registerPluginHandler: [registerPluginHandlerForOrder],
getDataForOrderEmail: [getDataForOrderEmail],
preStartup: [preStartup],
startup: [startup]
Expand Down
6 changes: 6 additions & 0 deletions packages/api-plugin-orders/src/mutations/placeOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import getAnonymousAccessToken from "@reactioncommerce/api-utils/getAnonymousAcc
import buildOrderFulfillmentGroupFromInput from "../util/buildOrderFulfillmentGroupFromInput.js";
import verifyPaymentsMatchOrderTotal from "../util/verifyPaymentsMatchOrderTotal.js";
import { Order as OrderSchema, orderInputSchema, Payment as PaymentSchema, paymentInputSchema } from "../simpleSchemas.js";
import { customOrderValidators } from "../registration.js";

const inputSchema = new SimpleSchema({
"order": orderInputSchema,
Expand Down Expand Up @@ -286,6 +287,11 @@ export default async function placeOrder(context, input) {

// Validate and save
OrderSchema.validate(order);

for (const customOrderValidateFunc of customOrderValidators) {
await customOrderValidateFunc.fn(context, order); // eslint-disable-line no-await-in-loop
}

await Orders.insertOne(order);

await appEvents.emit("afterOrderCreate", { createdBy: userId, order });
Expand Down
25 changes: 25 additions & 0 deletions packages/api-plugin-orders/src/registration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SimpleSchema from "simpl-schema";

const validatorSchema = new SimpleSchema({
name: String,
fn: Function
});

// Objects with `name` and `fn` properties
export const customOrderValidators = [];

/**
* @summary Will be called for every plugin
* @param {Object} options The options object that the plugin passed to registerPackage
* @returns {undefined}
*/
export function registerPluginHandlerForOrder({ name, order }) {
if (order) {
const { customValidators } = order;

if (!Array.isArray(customValidators)) throw new Error(`In ${name} plugin registerPlugin object, order.customValidators must be an array`);
validatorSchema.validate(customValidators);

customOrderValidators.push(...customValidators);
}
}
26 changes: 24 additions & 2 deletions packages/api-plugin-promotions-coupons/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import mutations from "./mutations/index.js";
import queries from "./queries/index.js";
import resolvers from "./resolvers/index.js";
import triggers from "./triggers/index.js";
import { Coupon } from "./simpleSchemas.js";
import { Coupon, CouponLog } from "./simpleSchemas.js";
import preStartupPromotionCoupon from "./preStartup.js";
import updateOrderCoupon from "./utils/updateOrderCoupon.js";

const require = createRequire(import.meta.url);
const pkg = require("../package.json");
Expand All @@ -26,8 +28,19 @@ export default async function register(app) {
[{ shopId: 1, code: 1 }],
[{ shopId: 1, promotionId: 1 }]
]
},
CouponLogs: {
name: "CouponLogs",
indexes: [
[{ couponId: 1 }],
[{ promotionId: 1 }],
[{ couponId: 1, accountId: 1 }, { unique: true }]
]
}
},
functionsByType: {
preStartup: [preStartupPromotionCoupon]
},
promotions: {
triggers
},
Expand All @@ -38,7 +51,16 @@ export default async function register(app) {
mutations,
queries,
simpleSchemas: {
Coupon
Coupon,
CouponLog
},
order: {
customValidators: [
{
name: "updateOrderCoupon",
fn: updateOrderCoupon
}
]
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import ReactionError from "@reactioncommerce/reaction-error";
import Logger from "@reactioncommerce/logger";
import hashToken from "@reactioncommerce/api-utils/hashToken.js";
import _ from "lodash";
import isPromotionExpired from "../utils/isPromotionExpired.js";

const inputSchema = new SimpleSchema({
shopId: String,
Expand All @@ -27,7 +26,7 @@ const inputSchema = new SimpleSchema({
export default async function applyCouponToCart(context, input) {
inputSchema.validate(input);

const { collections: { Cart, Promotions, Accounts }, userId } = context;
const { collections: { Cart, Promotions, Accounts, Coupons, CouponLogs }, userId } = context;
const { shopId, cartId, couponCode, cartToken } = input;

const selector = { shopId };
Expand All @@ -42,8 +41,8 @@ export default async function applyCouponToCart(context, input) {
const account = (userId && (await Accounts.findOne({ userId }))) || null;

if (!account) {
Logger.error(`Cart not found for user with ID ${userId}`);
throw new ReactionError("not-found", "Cart not found");
Logger.error(`Cart not found for user with ID ${account._id}`);
throw new ReactionError("invalid-params", "Cart not found");
}

selector.accountId = account._id;
Expand All @@ -52,33 +51,67 @@ export default async function applyCouponToCart(context, input) {
const cart = await Cart.findOne(selector);
if (!cart) {
Logger.error(`Cart not found for user with ID ${userId}`);
throw new ReactionError("not-found", "Cart not found");
throw new ReactionError("invalid-params", "Cart not found");
}

const now = new Date();
const coupons = await Coupons.find({
code: couponCode,
$or: [
{ expirationDate: { $gte: now } },
{ expirationDate: null }
]
}).toArray();
if (coupons.length > 1) {
throw new ReactionError("invalid-params", "The coupon have duplicate with other promotion. Please contact admin for more information");
}

if (coupons.length === 0) {
Logger.error(`The coupon code ${couponCode} is not found`);
throw new ReactionError("invalid-params", `The coupon ${couponCode} is not found`);
}

const coupon = coupons[0];

if (coupon.maxUsageTimes && coupon.maxUsageTimes > 0 && coupon.usedCount >= coupon.maxUsageTimes) {
Logger.error(`The coupon code ${couponCode} is expired`);
throw new ReactionError("invalid-params", "The coupon is expired");
}

if (coupon.maxUsageTimesPerUser && coupon.maxUsageTimesPerUser > 0) {
if (!userId) throw new ReactionError("invalid-params", "You must be logged in to apply this coupon");

const couponLog = await CouponLogs.findOne({ couponId: coupon._id, accountId: cart.accountId });
if (couponLog && couponLog.usedCount >= coupon.maxUsageTimesPerUser) {
Logger.error(`The coupon code ${couponCode} has expired`);
throw new ReactionError("invalid-params", "The coupon is expired");
}
}

const promotion = await Promotions.findOne({
"_id": coupon.promotionId,
shopId,
"enabled": true,
"type": "explicit",
"startDate": { $lte: now },
"triggers.triggerKey": "coupons",
"triggers.triggerParameters.couponCode": couponCode
"triggers.triggerKey": "coupons"
});

if (!promotion) {
Logger.error(`The promotion not found with coupon code ${couponCode}`);
throw new ReactionError("not-found", "The coupon is not available");
}

if (isPromotionExpired(promotion)) {
Logger.error(`The coupon code ${couponCode} is expired`);
throw new ReactionError("coupon-expired", "The coupon is expired");
throw new ReactionError("invalid-params", "The coupon is not available");
}

if (_.find(cart.appliedPromotions, { _id: promotion._id })) {
Logger.error(`The coupon code ${couponCode} is already applied`);
throw new Error("coupon-already-exists", "The coupon already applied on the cart");
throw new ReactionError("invalid-params", "The coupon already applied on the cart");
}

return context.mutations.applyExplicitPromotionToCart(context, cart, promotion);
const promotionWithCoupon = {
...promotion,
relatedCoupon: {
couponCode,
couponId: coupon._id
}
};

return context.mutations.applyExplicitPromotionToCart(context, cart, promotionWithCoupon);
}
Loading

0 comments on commit b698c5d

Please sign in to comment.