From e1353bd2324167681feeda646d773acf7a5786b6 Mon Sep 17 00:00:00 2001 From: vanpho93 Date: Tue, 27 Dec 2022 13:35:55 +0700 Subject: [PATCH] feat: remove coupon from cart mutation Signed-off-by: vanpho93 --- .../src/mutations/index.js | 4 +- .../src/mutations/removeCouponFromCart.js | 62 +++++++++++++ .../mutations/removeCouponFromCart.test.js | 91 +++++++++++++++++++ .../src/resolvers/Mutation/index.js | 4 +- .../Mutation/removeCouponFromCart.js | 14 +++ .../Mutation/removeCouponFromCart.test.js | 15 +++ .../src/schemas/schema.graphql | 74 ++++++++++++++- 7 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.js create mode 100644 packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.test.js create mode 100644 packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.js create mode 100644 packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.test.js diff --git a/packages/api-plugin-promotions-coupons/src/mutations/index.js b/packages/api-plugin-promotions-coupons/src/mutations/index.js index beaab1fbe59..9eb2b58e135 100644 --- a/packages/api-plugin-promotions-coupons/src/mutations/index.js +++ b/packages/api-plugin-promotions-coupons/src/mutations/index.js @@ -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 }; diff --git a/packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.js b/packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.js new file mode 100644 index 00000000000..aacb7444292 --- /dev/null +++ b/packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.js @@ -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} - 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; +} diff --git a/packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.test.js b/packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.test.js new file mode 100644 index 00000000000..731cb5e2bdc --- /dev/null +++ b/packages/api-plugin-promotions-coupons/src/mutations/removeCouponFromCart.test.js @@ -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({}); +}); diff --git a/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/index.js b/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/index.js index beaab1fbe59..9eb2b58e135 100644 --- a/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/index.js +++ b/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/index.js @@ -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 }; diff --git a/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.js b/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.js new file mode 100644 index 00000000000..4b432ff9ad5 --- /dev/null +++ b/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.js @@ -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} with updated cart + */ +export default async function removeCouponFromCart(_, { input }, context) { + const updatedCart = await context.mutations.removeCouponFromCart(context, input); + return { cart: updatedCart }; +} diff --git a/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.test.js b/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.test.js new file mode 100644 index 00000000000..a7c86dbf65e --- /dev/null +++ b/packages/api-plugin-promotions-coupons/src/resolvers/Mutation/removeCouponFromCart.test.js @@ -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); +}); diff --git a/packages/api-plugin-promotions-coupons/src/schemas/schema.graphql b/packages/api-plugin-promotions-coupons/src/schemas/schema.graphql index d500d89491a..75780164f0c 100644 --- a/packages/api-plugin-promotions-coupons/src/schemas/schema.graphql +++ b/packages/api-plugin-promotions-coupons/src/schemas/schema.graphql @@ -99,6 +99,67 @@ input CouponFilter { userId: ID } +"The input for the createStandardCoupon mutation" +input CreateStandardCouponInput { + "The shop ID" + shopId: ID! + + "The promotion ID" + promotionId: ID! + + "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 @@ -109,6 +170,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" @@ -176,9 +242,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 }