Skip to content

Commit

Permalink
feat: add remove coupon from cart mutation
Browse files Browse the repository at this point in the history
Signed-off-by: vanpho93 <vanpho02@gmail.com>
  • Loading branch information
vanpho93 committed Dec 29, 2022
1 parent 859f8a4 commit b161d79
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import applyCouponToCart from "./applyCouponToCart.js";
import createStandardCoupon from "./createStandardCoupon.js";
import removeCouponFromCart from "./removeCouponFromCart.js";

export default {
applyCouponToCart,
createStandardCoupon
createStandardCoupon,
removeCouponFromCart
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import SimpleSchema from "simpl-schema";
import ReactionError from "@reactioncommerce/reaction-error";
import Logger from "@reactioncommerce/logger";
import hashToken from "@reactioncommerce/api-utils/hashToken.js";
import _ from "lodash";

const inputSchema = new SimpleSchema({
shopId: String,
cartId: String,
promotionId: String,
cartToken: {
type: String,
optional: true
}
});

/**
* @summary Remove a coupon from a cart
* @param {Object} context - The application context
* @param {Object} input - The input
* @returns {Promise<Object>} - The updated cart
*/
export default async function removeCouponFromCart(context, input) {
inputSchema.validate(input);

const { collections: { Cart, Accounts }, userId } = context;
const { shopId, cartId, promotionId, cartToken } = input;

const selector = { shopId };

if (cartId) selector._id = cartId;

if (cartToken) {
selector.anonymousAccessToken = hashToken(cartToken);
} else {
const account = (userId && (await Accounts.findOne({ userId }))) || null;

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

selector.accountId = account._id;
}

const cart = await Cart.findOne(selector);
if (!cart) {
Logger.error(`Cart not found for user with ID ${userId}`);
throw new ReactionError("invalid-params", "Cart not found");
}

const newAppliedPromotions = _.filter(cart.appliedPromotions, (appliedPromotion) => appliedPromotion._id !== promotionId);
if (newAppliedPromotions.length === cart.appliedPromotions.length) {
Logger.error(`Promotion ${promotionId} not found on cart ${cartId}`);
throw new ReactionError("invalid-params", "Can't remove coupon because it's not on the cart");
}

cart.appliedPromotions = newAppliedPromotions;

const updatedCart = await context.mutations.saveCart(context, cart);
return updatedCart;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import mockContext from "@reactioncommerce/api-utils/tests/mockContext.js";
import removeCouponFromCart from "./removeCouponFromCart.js";

test("throws if validation check fails", async () => {
const input = { shopId: "123", cartId: "123" };

try {
await removeCouponFromCart(mockContext, input);
} catch (error) {
expect(error.error).toEqual("validation-error");
}
});

test("throws error when cart does not exist with userId", async () => {
const input = { shopId: "123", cartId: "123", promotionId: "promotionId" };
mockContext.collections = {
Accounts: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(null))
},
Cart: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(null))
}
};

try {
await removeCouponFromCart(mockContext, input);
} catch (error) {
expect(error.message).toEqual("Cart not found");
}
});

test("throws error when cart does not exist", async () => {
const input = { shopId: "123", cartId: "123", promotionId: "promotionId" };
const account = { _id: "accountId" };
mockContext.collections = {
Accounts: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(account))
},
Cart: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(null))
}
};

try {
await removeCouponFromCart(mockContext, input);
} catch (error) {
expect(error.error).toEqual("invalid-params");
expect(error.message).toEqual("Cart not found");
}
});

test("throws error when promotionId is not found on cart", async () => {
const input = { shopId: "123", cartId: "123", promotionId: "promotionId" };
const account = { _id: "accountId" };
const cart = { appliedPromotions: [{ _id: "promotionId2" }] };
mockContext.collections = {
Accounts: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(account))
},
Cart: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(cart))
}
};

try {
await removeCouponFromCart(mockContext, input);
} catch (error) {
expect(error.error).toEqual("invalid-params");
expect(error.message).toEqual("Can't remove coupon because it's not on the cart");
}
});

test("removes coupon from cart", async () => {
const input = { shopId: "123", cartId: "123", promotionId: "promotionId" };
const account = { _id: "accountId" };
const cart = { appliedPromotions: [{ _id: "promotionId" }] };
mockContext.collections = {
Accounts: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(account))
},
Cart: {
findOne: jest.fn().mockResolvedValueOnce(Promise.resolve(cart))
}
};
mockContext.mutations = {
saveCart: jest.fn().mockName("mutations.saveCart").mockReturnValueOnce(Promise.resolve({}))
};

const result = await removeCouponFromCart(mockContext, input);
expect(result).toEqual({});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ test("should call applyCouponToCart mutation", async () => {
shopId: "_shopId",
cartId: "_id",
couponCode: "CODE",
cartToken: "anonymousToken"
token: "anonymousToken"
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import applyCouponToCart from "./applyCouponToCart.js";
import createStandardCoupon from "./createStandardCoupon.js";
import removeCouponFromCart from "./removeCouponFromCart.js";

export default {
applyCouponToCart,
createStandardCoupon
createStandardCoupon,
removeCouponFromCart
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @method removeCouponFromCart
* @summary Apply a coupon to the cart
* @param {Object} _ unused
* @param {Object} args.input - The input arguments
* @param {Object} args.input.cartId - The cart ID
* @param {Object} args.input.couponCode - The promotion IDs
* @param {Object} context - The application context
* @returns {Promise<Object>} with updated cart
*/
export default async function removeCouponFromCart(_, { input }, context) {
const updatedCart = await context.mutations.removeCouponFromCart(context, input);
return { cart: updatedCart };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import mockContext from "@reactioncommerce/api-utils/tests/mockContext.js";
import removeCouponFromCart from "./removeCouponFromCart.js";

test("calls mutations.removeCouponFromCart and returns the result", async () => {
const input = { cartId: "123", couponCode: "CODE" };
const result = { _id: "123 " };
mockContext.mutations = {
removeCouponFromCart: jest.fn().mockName("mutations.removeCouponFromCart").mockReturnValueOnce(Promise.resolve(result))
};

const removedCoupon = await removeCouponFromCart(null, { input }, mockContext);

expect(removedCoupon).toEqual({ cart: result });
expect(mockContext.mutations.removeCouponFromCart).toHaveBeenCalledWith(mockContext, input);
});
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,70 @@ input CouponFilter {
userId: ID
}

"The input for the createStandardCoupon mutation"
input CreateStandardCouponInput {
"The shop ID"
shopId: ID!

"The promotion ID"
promotionId: ID!

"The coupon name"
name: String!

"The coupon code"
code: String!

"Can use this coupon in the store"
canUseInStore: Boolean!

"The number of times this coupon can be used per user"
maxUsageTimesPerUser: Int

"The number of times this coupon can be used"
maxUsageTimes: Int
}

input CouponQueryInput {
"The unique ID of the coupon"
_id: String!

"The unique ID of the shop"
shopId: String!
}

input CouponFilter {
"The expiration date of the coupon"
expirationDate: Date

"The related promotion ID"
promotionId: ID

"The coupon code"
code: String

"The coupon name"
userId: ID
}

"Input for the removeCouponFromCart mutation"
input RemoveCouponFromCartInput {

shopId: ID!

"The ID of the Cart"
cartId: ID!

"The promotion that contains the coupon to remove"
promotionId: ID!

"The account ID of the user who is applying the coupon"
accountId: ID

"Cart token, if anonymous"
token: String
}

"The response for the applyCouponToCart mutation"
type ApplyCouponToCartPayload {
cart: Cart
Expand All @@ -109,6 +173,11 @@ type StandardCouponPayload {
coupon: Coupon!
}

"The response for the removeCouponFromCart mutation"
type RemoveCouponFromCartPayload {
cart: Cart
}

"A connection edge in which each node is a `Coupon` object"
type CouponEdge {
"The cursor that represents this node in the paginated results"
Expand Down Expand Up @@ -176,9 +245,15 @@ extend type Mutation {
input: ApplyCouponToCartInput
): ApplyCouponToCartPayload

"Create a standard coupon mutation"
"Create a standard coupon mutation"
createStandardCoupon(
"The createStandardCoupon mutation input"
input: CreateStandardCouponInput
): StandardCouponPayload

"Remove a coupon from a cart"
removeCouponFromCart(
"The removeCouponFromCart mutation input"
input: RemoveCouponFromCartInput
): RemoveCouponFromCartPayload
}

0 comments on commit b161d79

Please sign in to comment.