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

Coupons feature branch #6769

Merged
merged 52 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e1e82a2
feat: create standard coupon mutation
vanpho93 Dec 29, 2022
f65959b
feat: improve apply coupon mutation
vanpho93 Dec 29, 2022
e1353bd
feat: remove coupon from cart mutation
vanpho93 Dec 27, 2022
db17285
Merge pull request #6735 from reactioncommerce/feat/improve-apply-cou…
vanpho93 Jan 30, 2023
13b876e
Merge pull request #6736 from reactioncommerce/feat/add-remove-coupon…
vanpho93 Jan 30, 2023
df9de17
Merge pull request #6734 from reactioncommerce/feat/create-standard-c…
vanpho93 Jan 30, 2023
343703a
feat: update coupon trigger parameter schema
vanpho93 Jan 30, 2023
1d83098
feat: add name field to coupon
vanpho93 Feb 1, 2023
65e9795
Merge pull request #6770 from reactioncommerce/feat/update-coupon-tri…
vanpho93 Feb 1, 2023
366a11a
feat: add additional coupon validation
vanpho93 Feb 1, 2023
729b4a5
feat: update promotion error message
vanpho93 Feb 6, 2023
193751c
Merge pull request #6771 from reactioncommerce/feat/add-additional-co…
vanpho93 Feb 7, 2023
da1de62
fix: add coupon to promotion
vannguyenn Feb 8, 2023
3fd4690
Merge pull request #6785 from reactioncommerce/fix/sample-data
brent-hoover Feb 8, 2023
f18b2e8
feat: create migration for old discoupon
vanpho93 Feb 1, 2023
919caa1
fix: add migration down method
vanpho93 Feb 9, 2023
ce0d117
feat: add archive coupon mutation
vanpho93 Feb 1, 2023
9db5e13
Merge pull request #6782 from reactioncommerce/feat/archive-coupon-mu…
vanpho93 Feb 10, 2023
f2a2321
fix: migration up error
vanpho93 Feb 10, 2023
93381c5
Merge pull request #6784 from reactioncommerce/feat/create-migration-…
brent-hoover Feb 13, 2023
58e29f2
Merge branch 'feat/promotions' into feat/coupons
vanpho93 Feb 14, 2023
4312af6
fix: merge issue
vanpho93 Feb 14, 2023
94501ed
Merge pull request #6800 from reactioncommerce/fix/merge-issue
vanpho93 Feb 14, 2023
4a614a3
fix: should query un-archived coupon in promotion
vannguyenn Feb 16, 2023
8da5f07
feat: shipping discount method
vanpho93 Feb 16, 2023
7d6032c
Merge pull request #6801 from reactioncommerce/fix/archived-coupon-pr…
brent-hoover Feb 16, 2023
b2297cd
fix: prevent applied coupon when is archived
vanpho93 Feb 14, 2023
d4578d7
Merge pull request #6804 from reactioncommerce/fix/prevent-applied-co…
brent-hoover Feb 20, 2023
fb551ee
feat: add calculate discount amount util
vanpho93 Feb 20, 2023
9e3feec
fix: place order with shipping discount
vanpho93 Feb 21, 2023
234c47e
fix: calculate shipping discount amount
vanpho93 Feb 21, 2023
7d9b8c1
feat: estimate discount amount for shipment quotes
vanpho93 Feb 22, 2023
88b213f
fix: applyPromotion unit test fail
vanpho93 Feb 24, 2023
3dcde45
fix: max discount value for shipping discount
vanpho93 Feb 24, 2023
f21dfaa
feat: add integration test for shipping disocunt
vanpho93 Feb 26, 2023
7bb460b
fix: unit test fail on disocuntAction
vanpho93 Feb 27, 2023
93a5c47
feat: add expect discount amount for integraiton test
vanpho93 Feb 27, 2023
070b284
feat: two shipping promotion test case
vanpho93 Feb 27, 2023
6be616a
feat: add additional redeemed coupon information
vanpho93 Feb 27, 2023
dc51fd5
fix: revert snyk
vanpho93 Feb 27, 2023
c88826b
feat: remove usedLogs field on CouponLog schema
vanpho93 Feb 28, 2023
5c6e241
Merge pull request #6816 from reactioncommerce/feat/add-additional-re…
brent-hoover Feb 28, 2023
dbb9c88
fix: temporary promotions
vanpho93 Feb 28, 2023
5769600
Merge branch 'feat/coupons' into feat/shipping-discounts
vanpho93 Mar 1, 2023
dd89dcd
fix: pnpm-lock file
vanpho93 Mar 1, 2023
6fbde96
feat: add sample data for shipping promotion
vanpho93 Mar 1, 2023
4072436
fix: promotion plugin unit test fail
vanpho93 Mar 1, 2023
686d7e2
fix: remove sampleData from plugin file
vanpho93 Mar 1, 2023
490044c
fix: checkout promotion test fail
vanpho93 Mar 1, 2023
c2471d3
feat: add check stackability for the shipping discount
vanpho93 Mar 2, 2023
36dd29c
fix: revert promotion starup file
vanpho93 Mar 2, 2023
e0c0e7c
Merge pull request #6802 from reactioncommerce/feat/shipping-discounts
vanpho93 Mar 3, 2023
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
Expand Up @@ -96,10 +96,10 @@ describe("Promotions", () => {
region: "CA"
};

const removeAllPromotions = async () => {
await testApp.setLoggedInUser(mockAdminAccount);
await testApp.collections.Promotions.remove({});
await testApp.clearLoggedInUser();
const cleanup = async () => {
await testApp.collections.Promotions.deleteMany();
await testApp.collections.Orders.deleteMany();
await testApp.collections.Cart.deleteMany();
};

const createTestPromotion = (overlay = {}) => {
Expand Down Expand Up @@ -253,7 +253,7 @@ describe("Promotions", () => {

describe("when a promotion is applied to an order with fixed promotion", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

createTestPromotion();
Expand All @@ -277,7 +277,7 @@ describe("Promotions", () => {

describe("when a promotion is applied to an order percentage discount", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

createTestPromotion({
Expand Down Expand Up @@ -321,7 +321,7 @@ describe("Promotions", () => {

describe("when a promotion applied via inclusion criteria", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

const triggerParameters = { ...fixedDiscountPromotion.triggers[0].triggerParameters };
Expand Down Expand Up @@ -401,7 +401,7 @@ describe("Promotions", () => {

describe("when a promotion isn't applied via inclusion criteria", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

const triggerParameters = { ...fixedDiscountPromotion.triggers[0].triggerParameters };
Expand Down Expand Up @@ -442,7 +442,7 @@ describe("Promotions", () => {

describe("when a promotion isn't applied by exclusion criteria", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

const triggerParameters = { ...fixedDiscountPromotion.triggers[0].triggerParameters };
Expand Down Expand Up @@ -500,7 +500,7 @@ describe("Promotions", () => {

describe("cart shouldn't contains any promotion when qualified promotion is change to disabled", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

createTestPromotion();
Expand Down Expand Up @@ -534,13 +534,13 @@ describe("Promotions", () => {
expect(cart.appliedPromotions).toHaveLength(0);
expect(cart.messages).toHaveLength(1);

await removeAllPromotions();
await cleanup();
});
});

describe("cart applied promotion with 10% but max discount is $20", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

createTestPromotion({
Expand Down Expand Up @@ -599,7 +599,7 @@ describe("Promotions", () => {

describe("Stackability: shouldn't stack with other promotion when stackability is none", () => {
afterAll(async () => {
await removeAllPromotions();
await cleanup();
});

createTestPromotion();
Expand All @@ -618,6 +618,10 @@ describe("Promotions", () => {
});

describe("Stackability: should applied with other promotions when stackability is all", () => {
afterAll(async () => {
await cleanup();
});

createTestPromotion();
createTestPromotion();
createTestCart({ quantity: 20 });
Expand All @@ -628,4 +632,93 @@ describe("Promotions", () => {
expect(cart.appliedPromotions).toHaveLength(2);
});
});

describe("apply with single shipping promotion", () => {
afterAll(async () => {
await cleanup();
});

createTestPromotion({
actions: [
{
actionKey: "discounts",
actionParameters: {
discountType: "shipping",
discountCalculationType: "percentage",
discountValue: 50
}
}
]
});

createCartAndPlaceOrder({ quantity: 6 });

test("placed order get the correct values", async () => {
const orderId = decodeOpaqueIdForNamespace("reaction/order")(placedOrderId);
const newOrder = await testApp.collections.Orders.findOne({ _id: orderId });
expect(newOrder.shipping[0].invoice.total).toEqual(121.94);
expect(newOrder.shipping[0].invoice.discounts).toEqual(0);
expect(newOrder.shipping[0].invoice.subtotal).toEqual(119.94);
expect(newOrder.shipping[0].invoice.shipping).toEqual(2);
expect(newOrder.shipping[0].shipmentMethod.discount).toEqual(0.5);
expect(newOrder.shipping[0].shipmentMethod.rate).toEqual(0.5);
expect(newOrder.shipping[0].shipmentMethod.handling).toEqual(1.5);

expect(newOrder.shipping[0].items[0].quantity).toEqual(6);

expect(newOrder.appliedPromotions[0]._id).toEqual(mockPromotion._id);
expect(newOrder.discounts).toHaveLength(1);
});
});

describe("apply with two shipping promotions", () => {
beforeAll(async () => {
await cleanup();
});

createTestPromotion({
label: "shipping promotion 1",
actions: [
{
actionKey: "discounts",
actionParameters: {
discountType: "shipping",
discountCalculationType: "percentage",
discountValue: 50
}
}
]
});

createTestPromotion({
label: "shipping promotion 2",
actions: [
{
actionKey: "discounts",
actionParameters: {
discountType: "shipping",
discountCalculationType: "percentage",
discountValue: 10
}
}
]
});

createCartAndPlaceOrder({ quantity: 6 });

test("placed order get the correct values", async () => {
const orderId = decodeOpaqueIdForNamespace("reaction/order")(placedOrderId);
const newOrder = await testApp.collections.Orders.findOne({ _id: orderId });
expect(newOrder.shipping[0].invoice.total).toEqual(121.89);
expect(newOrder.shipping[0].invoice.discounts).toEqual(0);
expect(newOrder.shipping[0].invoice.subtotal).toEqual(119.94);
expect(newOrder.shipping[0].invoice.shipping).toEqual(1.95);
expect(newOrder.shipping[0].shipmentMethod.discount).toEqual(0.55);
expect(newOrder.shipping[0].shipmentMethod.rate).toEqual(0.45);
expect(newOrder.shipping[0].shipmentMethod.handling).toEqual(1.5);

expect(newOrder.appliedPromotions).toHaveLength(2);
expect(newOrder.discounts).toHaveLength(2);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ const logCtx = { name: "cart", file: "transformAndValidateCart" };
* and validates it. Throws an error if invalid. The cart object is mutated.
* @param {Object} context - App context
* @param {Object} cart - The cart to transform and validate
* @param {Object} options - transform options
* @returns {undefined}
*/
export default async function transformAndValidateCart(context, cart) {
export default async function transformAndValidateCart(context, cart, options = {}) {
const { simpleSchemas: { Cart: cartSchema } } = context;
updateCartFulfillmentGroups(context, cart);

Expand Down Expand Up @@ -41,7 +42,7 @@ export default async function transformAndValidateCart(context, cart) {
await forEachPromise(cartTransforms, async (transformInfo) => {
const startTime = Date.now();
/* eslint-disable no-await-in-loop */
await transformInfo.fn(context, cart, { getCommonOrders });
await transformInfo.fn(context, cart, { getCommonOrders, ...options });
/* eslint-enable no-await-in-loop */
Logger.debug({ ...logCtx, cartId: cart._id, ms: Date.now() - startTime }, `Finished ${transformInfo.name} cart transform`);
});
Expand Down
7 changes: 5 additions & 2 deletions packages/api-plugin-carts/src/xforms/xformCartCheckout.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ function xformCartFulfillmentGroup(fulfillmentGroup, cart) {
displayName: fulfillmentGroup.shipmentMethod.label || fulfillmentGroup.shipmentMethod.name,
group: fulfillmentGroup.shipmentMethod.group || null,
name: fulfillmentGroup.shipmentMethod.name,
fulfillmentTypes: fulfillmentGroup.shipmentMethod.fulfillmentTypes
fulfillmentTypes: fulfillmentGroup.shipmentMethod.fulfillmentTypes,
discount: fulfillmentGroup.shipmentMethod.discount || 0,
undiscountedRate: fulfillmentGroup.shipmentMethod.rate || 0
},
handlingPrice: {
amount: fulfillmentGroup.shipmentMethod.handling || 0,
Expand All @@ -65,7 +67,8 @@ function xformCartFulfillmentGroup(fulfillmentGroup, cart) {
shippingAddress: fulfillmentGroup.address,
shopId: fulfillmentGroup.shopId,
// For now, this is always shipping. Revisit when adding download, pickup, etc. types
type: "shipping"
type: "shipping",
discounts: fulfillmentGroup.discounts || []
};
}

Expand Down
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
8 changes: 8 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 @@ -147,6 +148,8 @@ export default async function placeOrder(context, input) {
if (!allCartMessageAreAcknowledged) {
throw new ReactionError("invalid-cart", "Cart messages should be acknowledged before placing order");
}

await context.mutations.transformAndValidateCart(context, cart, { skipTemporaryPromotions: true });
}


Expand Down Expand Up @@ -286,6 +289,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
3 changes: 2 additions & 1 deletion packages/api-plugin-orders/src/mutations/placeOrder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ test("places an anonymous $0 order with no cartId and no payments", async () =>
group: undefined,
currencyCode: orderInput.currencyCode,
handling: 0,
rate: 0
rate: 0,
discount: 0
},
shopId: orderInput.shopId,
totalItemQuantity: 1,
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);
}
}
5 changes: 5 additions & 0 deletions packages/api-plugin-orders/src/simpleSchemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,11 @@ export const SelectedFulfillmentOption = new SimpleSchema({
rate: {
type: Number,
min: 0
},
discount: {
type: Number,
min: 0,
optional: true
}
});

Expand Down
11 changes: 9 additions & 2 deletions packages/api-plugin-orders/src/util/addShipmentMethodToGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ export default async function addShipmentMethodToGroup(context, {
throw new ReactionError("invalid", errorResult.message);
}

const { shipmentMethod: { rate: shipmentRate, undiscountedRate, discount, _id: shipmentMethodId } = {} } = group;
const selectedFulfillmentMethod = rates.find((rate) => selectedFulfillmentMethodId === rate.method._id);
if (!selectedFulfillmentMethod) {
const hasShipmentMethodObject = shipmentMethodId && shipmentMethodId !== selectedFulfillmentMethodId;
if (!selectedFulfillmentMethod || hasShipmentMethodObject) {
throw new ReactionError("invalid", "The selected fulfillment method is no longer available." +
" Fetch updated fulfillment options and try creating the order again with a valid method.");
}

if (undiscountedRate && undiscountedRate !== selectedFulfillmentMethod.rate) {
throw new ReactionError("invalid", "The selected fulfillment method has mismatch shipment rate.");
}

group.shipmentMethod = {
_id: selectedFulfillmentMethod.method._id,
carrier: selectedFulfillmentMethod.method.carrier,
Expand All @@ -59,6 +65,7 @@ export default async function addShipmentMethodToGroup(context, {
group: selectedFulfillmentMethod.method.group,
name: selectedFulfillmentMethod.method.name,
handling: selectedFulfillmentMethod.handlingPrice,
rate: selectedFulfillmentMethod.rate
rate: shipmentRate || selectedFulfillmentMethod.rate,
discount: discount || 0
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export default async function buildOrderFulfillmentGroupFromInput(context, {
if (Array.isArray(additionalItems) && additionalItems.length) {
group.items.push(...additionalItems);
}
if (cart && Array.isArray(cart.shipping)) {
const cartShipping = cart.shipping.find((shipping) => shipping.shipmentMethod?._id === selectedFulfillmentMethodId);
group.shipmentMethod = cartShipping?.shipmentMethod;
}

// Add some more properties for convenience
group.itemIds = group.items.map((item) => item._id);
Expand Down
2 changes: 2 additions & 0 deletions packages/api-plugin-promotions-coupons/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import register from "./src/index.js";

export { default as migrations } from "./migrations/index.js";

export default register;
Loading