Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
13 changes: 13 additions & 0 deletions imports/node-app/plugins/discount-codes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import getCreditOffDiscount from "./util/getCreditOffDiscount.js";
import getItemPriceDiscount from "./util/getItemPriceDiscount.js";
import getPercentageOffDiscount from "./util/getPercentageOffDiscount.js";
import getShippingDiscount from "./util/getShippingDiscount.js";
import mutations from "./mutations/index.js";
import resolvers from "./resolvers/index.js";
import schemas from "./schemas/index.js";
import startup from "./startup.js";

/**
Expand All @@ -22,6 +25,11 @@ export default async function register(app) {
"discounts/codes/shipping": [getShippingDiscount],
"startup": [startup]
},
graphQL: {
resolvers,
schemas
},
mutations,
registry: [
{
label: "Codes",
Expand All @@ -30,6 +38,11 @@ export default async function register(app) {
}, {
provides: ["paymentMethod"],
template: "discountCodesCheckout"
}, {
route: "discounts/apply",
label: "Apply Discounts",
permission: "applyDiscounts",
name: "discounts/apply"
}
]
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import SimpleSchema from "simpl-schema";
import Random from "@reactioncommerce/random";
import ReactionError from "@reactioncommerce/reaction-error";
import getCart from "../util/getCart.js";

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

/**
* @method applyDiscountCodeToCart
* @summary Applies a discount code to a cart
* @param {Object} context - an object containing the per-request state
* @param {Object} input - an object of all mutation arguments that were sent by the client
* @param {Object} input.cartId - Cart to add discount to
* @param {Object} input.discountCode - Discount code to add to cart
* @param {String} input.shopId - Shop cart belongs to
* @param {String} [input.token] - Cart token, if anonymous
* @returns {Promise<Object>} An object with the updated cart with the applied discount
*/
export default async function applyDiscountCodeToCart(context, input) {
inputSchema.validate(input);

const { cartId, discountCode, shopId, token } = input;
const { collections, userHasPermission, userId } = context;
const { Cart, Discounts } = collections;

let userCount = 0;
let orderCount = 0;
let cart = await getCart(context, shopId, cartId, { cartToken: token, throwIfNotFound: false });

// If we didn't find a cart, it means it belongs to another user,
// not the currently logged in user.
// Check to make sure current user has admin permission.
if (!cart) {
cart = await Cart.findOne({ _id: cartId });
if (!cart) {
throw new ReactionError("not-found", "Cart not found");
}

if (!userHasPermission(["owner", "admin", "discounts/apply"], shopId)) {
throw new ReactionError("access-denied", "Access Denied");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since discounts is more for defining/editing discounts, I'd create a new role discounts/apply for here.

}

const objectToApplyDiscount = cart;

const discount = await Discounts.findOne({ code: discountCode });
if (!discount) throw new ReactionError("not-found", `No discount found for code ${discountCode}`);

const { conditions } = discount;
let accountLimitExceeded = false;
let discountLimitExceeded = false;

// existing usage count
if (discount.transactions) {
const users = Array.from(discount.transactions, (trans) => trans.userId);
const transactionCount = new Map([...new Set(users)].map((userX) => [userX, users.filter((userY) => userY === userX).length]));
const orders = Array.from(discount.transactions, (trans) => trans.cartId);
userCount = transactionCount.get(userId);
orderCount = orders.length;
}
// check limits
if (conditions) {
if (conditions.accountLimit) accountLimitExceeded = conditions.accountLimit <= userCount;
if (conditions.redemptionLimit) discountLimitExceeded = conditions.redemptionLimit <= orderCount;
}

// validate basic limit handling
if (accountLimitExceeded === true || discountLimitExceeded === true) {
throw new ReactionError("error-occurred", "Code is expired");
}

if (!cart.billing) {
cart.billing = [];
}

cart.billing.push({
_id: Random.id(),
amount: discount.discount,
createdAt: new Date(),
currencyCode: objectToApplyDiscount.currencyCode,
data: {
discountId: discount._id,
code: discount.code
},
displayName: `Discount Code: ${discount.code}`,
method: discount.calculation.method,
mode: "discount",
name: "discount_code",
paymentPluginName: "discount-codes",
processor: discount.discountMethod,
shopId: objectToApplyDiscount.shopId,
status: "created",
transactionId: Random.id()
});

// Instead of directly updating cart, we add the discount billing
// object from the existing cart, then pass to `saveCart`
// to re-run cart through all transforms and validations.
const savedCart = await context.mutations.saveCart(context, cart);

return savedCart;
}
7 changes: 7 additions & 0 deletions imports/node-app/plugins/discount-codes/mutations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import applyDiscountCodeToCart from "./applyDiscountCodeToCart.js";
import removeDiscountCodeFromCart from "./removeDiscountCodeFromCart.js";

export default {
applyDiscountCodeToCart,
removeDiscountCodeFromCart
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import SimpleSchema from "simpl-schema";
import ReactionError from "@reactioncommerce/reaction-error";
import getCart from "../util/getCart.js";

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

/**
* @method removeDiscountCodeFromCart
* @summary Applies a discount code to a cart
* @param {Object} context - an object containing the per-request state
* @param {Object} input - an object of all mutation arguments that were sent by the client
* @param {Object} input.cartId - Cart to remove discount from
* @param {Object} input.discountId - Discount code to remove from cart
* @param {String} input.shopId - Shop cart belongs to
* @param {String} [input.token] - Cart token, if anonymous
* @returns {Promise<Object>} An object with the updated cart with the removed discount
*/
export default async function removeDiscountCodeFromCart(context, input) {
inputSchema.validate(input);

const { cartId, discountId, shopId, token } = input;
const { collections, userHasPermission } = context;
const { Cart } = collections;

let cart = await getCart(context, shopId, cartId, { cartToken: token, throwIfNotFound: false });

// If we didn't find a cart, it means it belongs to another user,
// not the currently logged in user.
// Check to make sure current user has admin permission.
if (!cart) {
if (!userHasPermission(["owner", "admin", "discounts/apply"], shopId)) {
throw new ReactionError("access-denied", "Access Denied");
}

cart = await Cart.findOne({ _id: cartId, shopId });
if (!cart) {
throw new ReactionError("not-found", "Cart not found");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you already have shopId, you can move this permission check above the Cart.findOne, but be sure to pass shopId into the findOne query.

}

// Instead of directly updating cart, we remove the discount billing
// object from the existing cart, then pass to `saveCart`
// to re-run cart through all transforms and validations.
const updatedCartBilling = cart.billing.filter((doc) => doc._id !== discountId);
if (cart.billing.length === updatedCartBilling.length) {
throw new ReactionError("not-found", "No applied discount found with that ID");
}
cart.billing = updatedCartBilling;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could add a check for whether cart.billing.length and updatedCartBilling.length are the same. If so, the discount code ID was invalid so throw an error.


const savedCart = await context.mutations.saveCart(context, cart);

return savedCart;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { decodeCartOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/cart";
import { decodeShopOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/shop";

/**
* @name Mutation/applyDiscountCodeToCart
* @method
* @memberof Fulfillment/GraphQL
* @summary resolver for the applyDiscountCodeToCart GraphQL mutation
* @param {Object} parentResult - unused
* @param {Object} args.input - an object of all mutation arguments that were sent by the client
* @param {Object} args.input.cartId - Cart to add discount to
* @param {Object} args.input.discountCode - Discount code to add to cart
* @param {String} args.input.shopId - Shop cart belongs to
* @param {String} [args.input.token] - Cart token, if anonymous
* @param {String} [args.input.clientMutationId] - An optional string identifying the mutation call
* @param {Object} context - an object containing the per-request state
* @returns {Promise<Object>} applyDiscountCodeToCartPayload
*/
export default async function applyDiscountCodeToCart(parentResult, { input }, context) {
const {
clientMutationId = null,
cartId,
discountCode,
shopId,
token
} = input;

const updatedCartWithAppliedDiscountCode = await context.mutations.applyDiscountCodeToCart(context, {
cartId: decodeCartOpaqueId(cartId),
discountCode,
shopId: decodeShopOpaqueId(shopId),
token
});

return {
clientMutationId,
cart: updatedCartWithAppliedDiscountCode
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import applyDiscountCodeToCart from "./applyDiscountCodeToCart.js";
import removeDiscountCodeFromCart from "./removeDiscountCodeFromCart.js";

export default {
applyDiscountCodeToCart,
removeDiscountCodeFromCart
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { decodeCartOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/cart";
import { decodeDiscountOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/discount";
import { decodeShopOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/shop";

/**
* @name Mutation/removeDiscountCodeFromCart
* @method
* @memberof Fulfillment/GraphQL
* @summary resolver for the removeDiscountCodeFromCart GraphQL mutation
* @param {Object} parentResult - unused
* @param {Object} args.input - an object of all mutation arguments that were sent by the client
* @param {Object} args.input.cartId - Cart to remove discount from
* @param {Object} args.input.discountId - Discount code Id to remove from cart
* @param {String} args.input.shopId - Shop cart belongs to
* @param {String} [args.input.token] - Cart token, if anonymous
* @param {String} [args.input.clientMutationId] - An optional string identifying the mutation call
* @param {Object} context - an object containing the per-request state
* @returns {Promise<Object>} removeDiscountCodeFromCartPayload
*/
export default async function removeDiscountCodeFromCart(parentResult, { input }, context) {
const {
clientMutationId = null,
cartId,
discountId,
shopId,
token
} = input;

const updatedCartWithRemovedDiscountCode = await context.mutations.removeDiscountCodeFromCart(context, {
cartId: decodeCartOpaqueId(cartId),
discountId: decodeDiscountOpaqueId(discountId),
shopId: decodeShopOpaqueId(shopId),
token
});

return {
clientMutationId,
cart: updatedCartWithRemovedDiscountCode
};
}
5 changes: 5 additions & 0 deletions imports/node-app/plugins/discount-codes/resolvers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Mutation from "./Mutation/index.js";

export default {
Mutation
};
3 changes: 3 additions & 0 deletions imports/node-app/plugins/discount-codes/schemas/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import schema from "./schema.graphql";

export default [schema];
61 changes: 61 additions & 0 deletions imports/node-app/plugins/discount-codes/schemas/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
extend type Mutation {
"Apply a discount code to a cart"
applyDiscountCodeToCart(
"Mutation input"
input: ApplyDiscountCodeToCartInput!
): ApplyDiscountCodeToCartPayload!

"Remove a discount code from a cart"
removeDiscountCodeFromCart(
"Mutation input"
input: RemoveDiscountCodeFromCartInput!
): RemoveDiscountCodeFromCartPayload!
}

"Input for an `ApplyDiscountCodeToCartInput`"
input ApplyDiscountCodeToCartInput {
"Cart to add discount to"
cartId: ID!

"Discount code to add to cart"
discountCode: String!

"Shop cart belongs to"
shopId: ID!

"Cart token, if anonymous"
token: String
}

"Input for an `RemoveDiscountCodeFromCartInput`"
input RemoveDiscountCodeFromCartInput {
"Cart to add discount to"
cartId: ID!

"ID of the discount you want to remove from the cart"
discountId: ID!

"Shop cart belongs to"
shopId: ID!

"Cart token, if anonymous"
token: String
}

"Response from the `applyDiscountCodeToCart` mutation"
type ApplyDiscountCodeToCartPayload {
"The updated cart with discount code applied"
cart: Cart!

"The same string you sent with the mutation params, for matching mutation calls with their responses"
clientMutationId: String
}

"Response from the `removeDiscountCodeFromCart` mutation"
type RemoveDiscountCodeFromCartPayload {
"The updated cart with discount code removed"
cart: Cart!

"The same string you sent with the mutation params, for matching mutation calls with their responses"
clientMutationId: String
}
Loading