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: improve promotion validation #6762

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 @@ -4,6 +4,7 @@ import Logger from "@reactioncommerce/logger";
import applyItemDiscountToCart from "../discountTypes/item/applyItemDiscountToCart.js";
import applyShippingDiscountToCart from "../discountTypes/shipping/applyShippingDiscountToCart.js";
import applyOrderDiscountToCart from "../discountTypes/order/applyOrderDiscountToCart.js";
import { DiscountActionCondition } from "../simpleSchemas.js";

const require = createRequire(import.meta.url);

Expand All @@ -22,13 +23,6 @@ const functionMap = {
order: applyOrderDiscountToCart
};

export const Rules = new SimpleSchema({
conditions: {
type: Object,
blackbox: true
}
});

export const discountActionParameters = new SimpleSchema({
discountType: {
type: String,
Expand All @@ -50,10 +44,11 @@ export const discountActionParameters = new SimpleSchema({
optional: true
},
inclusionRules: {
type: Rules
type: DiscountActionCondition,
optional: true
},
exclusionRules: {
type: Rules,
type: DiscountActionCondition,
optional: true
},
neverStackWithOtherItemLevelDiscounts: {
Expand Down Expand Up @@ -101,7 +96,6 @@ export async function discountActionHandler(context, cart, params) {

const { cart: updatedCart, affected, reason } = await functionMap[discountType](context, params, cart);


Logger.info({ ...logCtx, ...params.actionParameters, cartId: cart._id, cartDiscount: cart.discount }, "Completed applying Discount to Cart");
return { updatedCart, affected, reason };
}
Expand Down
15 changes: 14 additions & 1 deletion packages/api-plugin-promotions-discounts/src/preStartup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SimpleSchema from "simpl-schema";
import { CartDiscount } from "./simpleSchemas.js";
import { CartDiscount, ConditionRule } from "./simpleSchemas.js";

const discountSchema = new SimpleSchema({
// this is here for backwards compatibility with old discounts
Expand Down Expand Up @@ -180,4 +180,17 @@ async function extendOrderSchemas(context) {
export default async function preStartupDiscounts(context) {
await extendCartSchemas(context);
await extendOrderSchemas(context);

const { promotionOfferFacts, promotions: { allowOperators } } = context;

const promotionFactKeys = Object.keys(promotionOfferFacts);

ConditionRule.extend({
fact: {
allowedValues: ConditionRule.getAllowedValuesForKey("fact").concat(promotionFactKeys)
},
operator: {
allowedValues: allowOperators
}
});
}
65 changes: 60 additions & 5 deletions packages/api-plugin-promotions-discounts/src/simpleSchemas.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,64 @@
import SimpleSchema from "simpl-schema";

export const Rules = new SimpleSchema({
conditions: {
const allowOperators = [
"equal",
"notEqual",
"lessThan",
"lessThanInclusive",
"greaterThan",
"greaterThanInclusive",
"in",
"notIn",
"contains",
"doesNotContain"
];

export const ConditionRule = new SimpleSchema({
"fact": {
type: String,
allowedValues: ["cart", "item"]
},
"operator": {
type: String,
allowedValues: allowOperators
},
"path": {
type: String,
optional: true
},
"value": {
type: SimpleSchema.oneOf(String, Number, Boolean, Array)
},
"value.$": {
type: SimpleSchema.oneOf(String, Number, Boolean)
},
"params": {
type: Object,
blackbox: true
blackbox: true,
optional: true
}
});

export const RuleExpression = new SimpleSchema({
"all": {
type: Array,
optional: true
},
"all.$": {
type: ConditionRule
},
"any": {
type: Array,
optional: true
},
"any.$": {
type: ConditionRule
}
});

export const DiscountActionCondition = new SimpleSchema({
conditions: {
type: RuleExpression
}
});

Expand Down Expand Up @@ -40,10 +95,10 @@ export const Discount = new SimpleSchema({
type: Number
},
inclusionRules: {
type: Rules
type: DiscountActionCondition
},
exclusionRules: {
type: Rules,
type: DiscountActionCondition,
optional: true
}
});
Expand Down
8 changes: 7 additions & 1 deletion packages/api-plugin-promotions-offers/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import triggers from "./triggers/index.js";
import enhancers from "./enhancers/index.js";
import facts from "./facts/index.js";
import { promotionOfferFacts, registerPromotionOfferFacts } from "./registration.js";
import { ConditionRule } from "./simpleSchemas.js";
import preStartupPromotionOffer from "./preStartup.js";

const require = createRequire(import.meta.url);
const pkg = require("../package.json");
Expand All @@ -18,6 +20,7 @@ export default async function register(app) {
name: pkg.name,
version: pkg.version,
functionsByType: {
preStartup: [preStartupPromotionOffer],
registerPluginHandler: [registerPromotionOfferFacts]
},
contextAdditions: {
Expand All @@ -27,6 +30,9 @@ export default async function register(app) {
triggers,
enhancers
},
promotionOfferFacts: facts
promotionOfferFacts: facts,
simpleSchemas: {
ConditionRule
}
});
}
21 changes: 21 additions & 0 deletions packages/api-plugin-promotions-offers/src/preStartup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ConditionRule } from "./simpleSchemas.js";

/**
* @summary Pre-startup function for api-plugin-promotions-offer
* @param {Object} context - Startup context
* @returns {Promise<void>} undefined
*/
export default async function preStartupPromotionOffer(context) {
const { promotionOfferFacts, promotions: { allowOperators } } = context;

const promotionFactKeys = Object.keys(promotionOfferFacts);

ConditionRule.extend({
fact: {
allowedValues: ConditionRule.getAllowedValuesForKey("fact").concat(promotionFactKeys)
},
operator: {
allowedValues: allowOperators
}
});
}
68 changes: 61 additions & 7 deletions packages/api-plugin-promotions-offers/src/simpleSchemas.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,78 @@
import SimpleSchema from "simpl-schema";

const Rules = new SimpleSchema({
conditions: {
const allowOperators = [
"equal",
"notEqual",
"lessThan",
"lessThanInclusive",
"greaterThan",
"greaterThanInclusive",
"in",
"notIn",
"contains",
"doesNotContain"
];

export const ConditionRule = new SimpleSchema({
"fact": {
type: String,
allowedValues: ["cart", "item"]
},
"operator": {
type: String,
allowedValues: allowOperators
},
"path": {
type: String,
optional: true
},
"value": {
type: SimpleSchema.oneOf(String, Number, Boolean, Array)
},
"value.$": {
type: SimpleSchema.oneOf(String, Number, Boolean)
},
"params": {
type: Object,
blackbox: true
blackbox: true,
optional: true
}
});

export const RuleExpression = new SimpleSchema({
"all": {
type: Array,
optional: true
},
"all.$": {
type: ConditionRule
},
"any": {
type: Array,
optional: true
},
"any.$": {
type: ConditionRule
}
});

export const OfferTriggerCondition = new SimpleSchema({
conditions: {
type: RuleExpression
}
});

export const OfferTriggerParameters = new SimpleSchema({
name: String,
conditions: {
type: Object,
blackbox: true
type: RuleExpression
},
inclusionRules: {
type: Rules,
type: OfferTriggerCondition,
optional: true
},
exclusionRules: {
type: Rules,
type: OfferTriggerCondition,
optional: true
}
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Random from "@reactioncommerce/random";
import validateActionParams from "./validateActionParams.js";
import validateTriggerParams from "./validateTriggerParams.js";

/**
Expand All @@ -21,8 +22,11 @@ export default async function createPromotion(context, promotion) {
promotion.createdAt = now;
promotion.updatedAt = now;
promotion.referenceId = await context.mutations.incrementSequence(context, promotion.shopId, "Promotions");

PromotionSchema.validate(promotion);
validateTriggerParams(context, promotion);
validateActionParams(context, promotion);

const results = await Promotions.insertOne(promotion);
const { insertedCount, insertedId } = results;
promotion._id = insertedId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,56 @@ const offerTrigger = {
type: "implicit"
};

const discountActionParameters = new SimpleSchema({
discountType: {
type: String,
allowedValues: ["item", "order", "shipping"]
},
discountCalculationType: {
type: String,
allowedValues: ["flat", "fixed", "percentage"]
},
discountValue: {
type: Number
},
discountMaxValue: {
type: Number,
optional: true
},
discountMaxUnits: {
type: Number,
optional: true
},
inclusionRules: {
type: Object,
blackbox: true,
optional: true
},
exclusionRules: {
type: Object,
blackbox: true,
optional: true
},
neverStackWithOtherItemLevelDiscounts: {
type: Boolean,
optional: true,
defaultValue: false
}
});

const discountAction = {
key: "discounts",
handler: () => {},
paramSchema: discountActionParameters
};


mockContext.promotions = {
triggers: [
offerTrigger
],
actions: [
discountAction
]
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ export const CreateOrderPromotion = {
],
actions: [
{
actionKey: "noop",
actionParameters: {}
actionKey: "discounts",
actionParameters: {
discountType: "order",
discountCalculationType: "percentage",
discountValue: 5
}
}
],
startDate: now,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @summary validate the parameters of the particular action
* @param {Object} context - The application context
* @param {Object} promotion - The promotion to validate
* @returns {undefined} throws error if invalid
*/
export default function validateActionParams(context, promotion) {
const { promotions } = context;
for (const action of promotion.actions) {
const actionData = promotions.actions.find((ac) => ac.key === action.actionKey);
const { paramSchema } = actionData;
paramSchema.validate(action.actionParameters);
}
}
Loading