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: remove coupon from cart mutation #6736

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
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
@@ -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,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
Expand All @@ -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"
Expand Down Expand Up @@ -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
}