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: shipping discount #6802

Merged
merged 20 commits into from
Mar 3, 2023
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
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/mutations/placeOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,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
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
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
11 changes: 2 additions & 9 deletions packages/api-plugin-promotions-coupons/src/preStartup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import _ from "lodash";
import SimpleSchema from "simpl-schema";
import doesDatabaseVersionMatch from "@reactioncommerce/db-version-check";
import { migrationsNamespace } from "../migrations/migrationsNamespace.js";
Expand All @@ -12,7 +11,7 @@ const expectedVersion = 2;
* @returns {undefined}
*/
export default async function preStartupPromotionCoupon(context) {
const { simpleSchemas: { Cart, Promotion, RuleExpression }, promotions: pluginPromotions } = context;
const { simpleSchemas: { RuleExpression, CartPromotionItem }, promotions: pluginPromotions } = context;

CouponTriggerCondition.extend({
conditions: RuleExpression
Expand All @@ -26,24 +25,18 @@ export default async function preStartupPromotionCoupon(context) {
const offerTrigger = pluginPromotions.triggers.find((trigger) => trigger.key === "offers");
if (!offerTrigger) throw new Error("No offer trigger found. Need to register offers trigger first.");

const copiedPromotion = _.cloneDeep(Promotion);

const relatedCoupon = new SimpleSchema({
couponCode: String,
couponId: String
});

copiedPromotion.extend({
CartPromotionItem.extend({
relatedCoupon: {
type: relatedCoupon,
optional: true
}
});

Cart.extend({
"appliedPromotions.$": copiedPromotion
});

const setToExpectedIfMissing = async () => {
const anyDiscount = await context.collections.Discounts.findOne();
return !anyDiscount;
Expand Down
Loading