Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve apply coupon mutation #6735

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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