diff --git a/packages/app-elements/src/providers/I18NProvider.tsx b/packages/app-elements/src/providers/I18NProvider.tsx index 67beb37d0..0ab6e24a0 100644 --- a/packages/app-elements/src/providers/I18NProvider.tsx +++ b/packages/app-elements/src/providers/I18NProvider.tsx @@ -91,6 +91,7 @@ const initI18n = async ( load: "languageOnly", lng: localeCode, fallbackLng: i18nLocales[0], + showSupportNotice: false, react: { useSuspense: true, }, diff --git a/packages/app-elements/src/ui/forms/CodeEditor/CodeEditorComponent.tsx b/packages/app-elements/src/ui/forms/CodeEditor/CodeEditorComponent.tsx index 8a30c92a7..c9aa45d41 100644 --- a/packages/app-elements/src/ui/forms/CodeEditor/CodeEditorComponent.tsx +++ b/packages/app-elements/src/ui/forms/CodeEditor/CodeEditorComponent.tsx @@ -178,7 +178,6 @@ export const CodeEditor = forwardRef< const schema = await fetchJsonSchema(jsonSchema, domain).then( (json) => { if (json != null) { - console.log(json) return clearExamples(json) } diff --git a/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx b/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx index 148a5ceca..8cabf946a 100644 --- a/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx +++ b/packages/app-elements/src/ui/forms/InputSelect/overrides.tsx @@ -30,7 +30,7 @@ function DropdownIndicator( {/** biome-ignore lint/a11y/noSvgWithoutTitle: Don't need to add a title */} const typeDictionary: Record = { + free_gift: "Free gift", percentage: "Percentage discount", fixed_amount: "Fixed amount", fixed_price: "Fixed price", @@ -61,6 +55,67 @@ export function ActionListItem({ const pathPrefix = `rules.${selectedRuleIndex}.actions.${index}` + const setDefaultOptionFor = (optionName: string) => { + switch (optionName) { + case "selector": + setPath( + `${pathPrefix}.selector`, + schemaType === "order-rules" + ? "order.line_items" + : schemaType === "price-rules" + ? "price" + : null, + true, + ) + break + case "groups": + setPath(`${pathPrefix}.groups`, []) + break + case "round": + setPath(`${pathPrefix}.round`, true) + break + case "apply_on": + setPath(`${pathPrefix}.apply_on`, null, true) + break + case "discount_mode": + setPath(`${pathPrefix}.discount_mode`, "default") + break + case "limit": + setPath(`${pathPrefix}.limit`, {}) + break + case "aggregation": + setPath(`${pathPrefix}.aggregation`, {}) + break + case "bundle": + setPath(`${pathPrefix}.bundle`, { + type: "balanced", + }) + break + case "quantity": + setPath(`${pathPrefix}.quantity`, null, true) + break + case "identifiers": { + setPath(`${pathPrefix}.identifiers`, {}) + break + } + } + } + + useEffect( + function ensureRequiredOptions() { + if (item == null) { + return + } + + requiredOptions.forEach((option) => { + if (!(option.name in item)) { + setDefaultOptionFor(option.name) + } + }) + }, + [item, requiredOptions, pathPrefix], + ) + return (
- - - + {availableOptions.length > 0 && ( { // Set default values based on option type - switch (option.name) { - case "round": - setPath(`${pathPrefix}.round`, true) - break - case "apply_on": - setPath(`${pathPrefix}.apply_on`, null, true) - break - case "discount_mode": - setPath(`${pathPrefix}.discount_mode`, "default") - break - case "limit": - setPath(`${pathPrefix}.limit`, {}) - break - case "aggregation": - setPath(`${pathPrefix}.aggregation`, {}) - break - case "bundle": - setPath(`${pathPrefix}.bundle`, { - type: "balanced", - }) - break - } + setDefaultOptionFor(option.name) }} label={option.label} key={`option-${option.name}`} @@ -151,7 +183,9 @@ export function ActionListItem({ }))} onSelect={(selected) => { if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.type`, selected.value) + setPath(`${pathPrefix}`, { + type: selected.value, + }) } }} /> @@ -163,132 +197,3 @@ export function ActionListItem({
) } - -function ActionSelector({ - item, - pathPrefix, -}: { - item: SchemaActionItem | null - pathPrefix: string -}) { - const { setPath, schemaType } = useRuleEngine() - const { t } = useTranslation() - - const initialValues = actionPaths[schemaType].map((field) => ({ - value: field, - label: (t(`resource_paths.${field}`) as string).replace( - "resource_paths.", - "", - ), - })) - - const name = `${pathPrefix}.selector` - - return ( - - Apply to - - } - > - c.value === item.selector) ?? { - value: item.selector, - label: item.selector, - }) - } - onSelect={async (selection) => { - if (isSingleValueSelected(selection)) { - setPath(name, selection.value) - setPath(`${pathPrefix}.apply_on`, null) - } - }} - /> - - ) -} - -function ActionGroups({ - item, - pathPrefix, -}: { - item: SchemaActionItem | null - pathPrefix: string -}) { - const { setPath, schemaType } = useRuleEngine() - const availableGroups = useAvailableGroups() - - useEffect(() => { - if (availableGroups.length === 0 && (item?.groups ?? []).length > 0) { - setPath(`${pathPrefix}.groups`, []) - } - }, [availableGroups]) - - if (availableGroups.length <= 0 && (item?.groups ?? []).length <= 0) { - return null - } - - return ( - - Groups - - } - > - ({ - label: availableGroups.includes(groups) - ? groups - : `⚠️   ${groups}`, - value: groups, - })) - : undefined - } - initialValues={availableGroups.map((group) => ({ - value: group, - label: group, - }))} - onSelect={(selected) => { - if (isMultiValueSelected(selected)) { - setPath( - `${pathPrefix}.groups`, - selected.map((s) => s.value), - ) - - if (schemaType === "order-rules" && selected.length > 0) { - setPath(`${pathPrefix}.selector`, "order.line_items") - } - } - }} - /> - - ) -} - -const actionPaths = { - "order-rules": [ - "order", - "order.line_items", - "order.line_items.line_item_options", - "order.line_items.sku", - "order.line_items.bundle", - "order.line_items.shipment", - "order.line_items.payment_method", - "order.line_items.adjustment", - "order.line_items.gift_card", - ] as const, - "price-rules": ["price"] as const, -} satisfies Record diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Action/ActionValue.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Action/ActionValue.tsx index 55702cf4c..de2765f75 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/Action/ActionValue.tsx +++ b/packages/app-elements/src/ui/forms/RuleEngine/Action/ActionValue.tsx @@ -19,6 +19,7 @@ export function ActionValue({ return (
{ setPath( `rules.${selectedRuleIndex}.actions.${actions?.length ?? 0}`, - { - selector: "order", - }, + null, + true, ) }} > diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Condition/ConditionListItem.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Condition/ConditionListItem.tsx index fe1258ac1..82a975be6 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/Condition/ConditionListItem.tsx +++ b/packages/app-elements/src/ui/forms/RuleEngine/Condition/ConditionListItem.tsx @@ -5,7 +5,7 @@ import { Text } from "#ui/atoms/Text" import { Dropdown, DropdownDivider, DropdownItem } from "#ui/composite/Dropdown" import { InputResourcePath } from "../InputResourcePath" import { ListItemContainer } from "../layout/ListItemContainer" -import { Options } from "../Options" +import { ConditionOptions } from "../Options" import { useAvailableOptions } from "../optionsConfig" import { useRuleEngine } from "../RuleEngineContext" import type { SchemaConditionItem } from "../utils" @@ -80,7 +80,7 @@ export function ConditionListItem({ !isEmpty(item?.matcher) && ( <> - + {availableOptions.length > 0 && ( ["infos"] onSelect?: (selected: PossibleSelectValue) => void }> = ({ value, pathKey, infos, onSelect }) => { - const { sdkClient } = useCoreSdkProvider() - const { setPath } = useRuleEngine() - const key = infos?.field?.name ?? "id" - - const resource = getResourceType(infos?.resource?.id) - - const { data } = useCoreApi( - resource, - "list", - infos?.resource?.id == null ? null : [getParams({ value: "" })], - ) - - const { data: selectedData, isLoading: isLoadingSelectedData } = useCoreApi( - resource, - "list", - infos?.resource?.id == null - ? null - : [ - { - ...getParams({ value: "" }), - filters: { - id_in: Array.isArray(value) ? value : [value], - }, - }, - ], - ) - - const initialValues = uniqBy([...(selectedData ?? []), ...(data ?? [])], "id") - - function getValue(value: ItemWithValue["value"]): InputSelectValue { - return { - label: - initialValues?.find((item) => item.id === value.toString())?.name ?? - `${isLoadingSelectedData ? "" : "⚠️ "}${value.toString()}`, - value: value.toString(), - } - } - return ( - 25 - ? "Type to search for more options." - : undefined - } - initialValues={toInputSelectValues(initialValues ?? [], key)} - loadAsyncValues={async (value) => { - const items = await sdkClient[resource].list(getParams({ value })) - - return toInputSelectValues(items, key) - }} + value={value} onSelect={(selected) => { onSelect?.(selected) if (isMultiValueSelected(selected)) { @@ -101,26 +41,3 @@ export const InputResourceSelector: React.FC<{ /> ) } - -function getParams({ value }: { value: string }): QueryParamsList { - return { - pageSize: 25, - sort: { - name: "asc", - }, - filters: { - name_cont: value, - }, - } -} - -function toInputSelectValues( - items: Array<{ name: string }>, - key: string, -): InputSelectValue[] { - return items.map((item) => ({ - label: item.name, - // @ts-expect-error TODO: fix this - value: item[key], - })) -} diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Condition/hooks.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Condition/hooks.tsx index d7ed4550b..90dcd4ce4 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/Condition/hooks.tsx +++ b/packages/app-elements/src/ui/forms/RuleEngine/Condition/hooks.tsx @@ -1,4 +1,4 @@ -import type { ResourceTypeLock } from "@commercelayer/sdk" +import type { ListableResourceType } from "@commercelayer/sdk" import { compact, uniq } from "lodash-es" import { useEffect, useState } from "react" import { atPath } from "#ui/forms/CodeEditor/fetchCoreResourcesSuggestions" @@ -12,7 +12,8 @@ const selectableResources = { tag: "tags", sku: "skus", sku_list: "sku_lists", -} as const satisfies Record + bundle: "bundles", +} as const satisfies Record export function getResourceType(resourceId: string | undefined) { return selectableResources[resourceId as keyof typeof selectableResources] diff --git a/packages/app-elements/src/ui/forms/RuleEngine/InputResourceSelector.tsx b/packages/app-elements/src/ui/forms/RuleEngine/InputResourceSelector.tsx new file mode 100644 index 000000000..d1ff1a277 --- /dev/null +++ b/packages/app-elements/src/ui/forms/RuleEngine/InputResourceSelector.tsx @@ -0,0 +1,115 @@ +import type { + ListableResourceType, + QueryParamsList, + Tag, +} from "@commercelayer/sdk" +import { uniqBy } from "lodash-es" +import type React from "react" +import { useCoreApi, useCoreSdkProvider } from "#providers/CoreSdkProvider" +import { + InputSelect, + type InputSelectValue, + type PossibleSelectValue, +} from "#ui/forms/InputSelect" +import type { ItemWithValue } from "./utils" + +export const InputResourceSelector: React.FC<{ + value?: ItemWithValue["value"] + resource: Extract< + ListableResourceType, + "markets" | "tags" | "skus" | "sku_lists" | "bundles" + > + resourceKey: string + isMulti?: boolean + onSelect: (selected: PossibleSelectValue) => void + isDisabled?: boolean +}> = ({ + value, + resource, + isDisabled, + resourceKey, + onSelect, + isMulti = false, +}) => { + const { sdkClient } = useCoreSdkProvider() + + const { data } = useCoreApi(resource, "list", [getParams({ value: "" })]) + + const { data: selectedData, isLoading: isLoadingSelectedData } = useCoreApi( + resource, + "list", + [ + { + ...getParams({ value: "" }), + filters: { + id_in: Array.isArray(value) ? value : [value], + }, + }, + ], + ) + + const initialValues = uniqBy([...(selectedData ?? []), ...(data ?? [])], "id") + + function getValue(value: ItemWithValue["value"]): InputSelectValue { + return { + label: + initialValues?.find((item) => item.id === value.toString())?.name ?? + `${isLoadingSelectedData ? "" : "⚠️ "}${value.toString()}`, + value: value.toString(), + } + } + + return ( + 25 + ? "Type to search for more options." + : undefined + } + initialValues={toInputSelectValues(initialValues ?? [], resourceKey)} + loadAsyncValues={async (value) => { + const items = await sdkClient[resource].list(getParams({ value })) + + return toInputSelectValues(items, resourceKey) + }} + onSelect={(selected) => { + onSelect?.(selected) + }} + /> + ) +} + +function getParams({ value }: { value: string }): QueryParamsList { + return { + pageSize: 25, + sort: { + name: "asc", + }, + filters: { + name_cont: value, + }, + } +} + +function toInputSelectValues( + items: Array<{ name: string }>, + key: string, +): InputSelectValue[] { + return items.map((item) => ({ + label: item.name, + // @ts-expect-error TODO: fix this + value: item[key], + })) +} diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Options/ActionOptions.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Options/ActionOptions.tsx new file mode 100644 index 000000000..fcf665337 --- /dev/null +++ b/packages/app-elements/src/ui/forms/RuleEngine/Options/ActionOptions.tsx @@ -0,0 +1,641 @@ +import { isEqual } from "lodash-es" +import { useTranslation } from "react-i18next" +import { Text } from "#ui/atoms/Text" +import { Input } from "#ui/forms/Input" +import { + InputSelect, + isMultiValueSelected, + isSingleValueSelected, +} from "#ui/forms/InputSelect" +import { useAvailableGroups } from "../Condition/hooks" +import { InputResourceSelector } from "../InputResourceSelector" +import { OptionRow } from "../layout/OptionRow" +import type { RuleEngineProps } from "../RuleEngineComponent" +import { useRuleEngine } from "../RuleEngineContext" +import type { SchemaActionItem } from "../utils" +import { AggregationRow, useOptionRow } from "./common" + +export function ActionOptions({ + item, + pathPrefix, +}: { + item: SchemaActionItem | null + pathPrefix: string +}) { + if (item == null) { + return null + } + + return ( + <> + + + + + + + + + + + + ) +} + +function SelectorOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "selector" as const + + const { setPath, schemaType } = useRuleEngine() + const { t } = useTranslation() + + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + const initialValues = actionPaths[schemaType].map((field) => ({ + value: field, + label: (t(`resource_paths.${field}`) as string).replace( + "resource_paths.", + "", + ), + })) + + const name = `${pathPrefix}.${optionName}` + + const value = + optionName in item + ? item?.selector == null + ? undefined + : (initialValues.find((c) => c.value === item.selector) ?? { + value: item.selector, + label: item.selector, + }) + : undefined + + return ( + + { + if (isSingleValueSelected(selection)) { + setPath(name, selection.value) + setPath(`${pathPrefix}.apply_on`, null) + } + }} + /> + + ) +} + +function GroupsOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "groups" as const + + const { setPath, schemaType } = useRuleEngine() + const availableGroups = useAvailableGroups() + + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + if ( + availableGroups.length <= 0 && + ("groups" in item && item.groups != null ? item.groups : []).length <= 0 + ) { + return null + } + + const value = + "groups" in item + ? item != null + ? item.groups?.map((groups) => ({ + label: availableGroups.includes(groups) ? groups : `⚠️   ${groups}`, + value: groups, + })) + : undefined + : undefined + + return ( + + ({ + value: group, + label: group, + }))} + onSelect={(selected) => { + if (isMultiValueSelected(selected)) { + setPath( + `${pathPrefix}.groups`, + selected.map((s) => s.value), + ) + + if (schemaType === "order-rules" && selected.length > 0) { + setPath(`${pathPrefix}.selector`, "order.line_items") + } + } + }} + /> + + ) +} + +function RoundOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "round" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + const initialValues = [ + { label: "Yes", value: true }, + { label: "No", value: false }, + ] + + const defaultValue = initialValues.find((v) => v.value === item[optionName]) + + return ( + + { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}`, selected.value) + } + }} + /> + + ) +} + +function QuantityOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "quantity" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + const defaultValue = item[optionName] + + return ( + + { + const value = parseInt(e.currentTarget.value, 10) + setPath(`${pathPrefix}.${optionName}`, value) + }} + /> + + ) +} + +function LimitOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "limit" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + const defaultValue = optionRow.optionConfig?.values?.find((entry) => + isEqual(item.limit?.sort, entry.meta), + ) + + return ( + +
+ { + const value = parseInt(e.currentTarget.value, 10) + setPath(`${pathPrefix}.${optionName}.value`, value) + }} + defaultValue={item.limit?.value} + /> + ({ + label: entry.label, + meta: entry.meta, + value: JSON.stringify(entry.meta), + })) ?? [] + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}.sort`, selected.meta) + } + }} + /> +
+
+ ) +} + +function AggregationOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "aggregation" as const + + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null || item[optionName] == null) { + return null + } + + return ( + + + + ) +} + +function BundleOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "bundle" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + const defaultValue = optionRow.optionConfig?.values?.find((entry) => + isEqual(item.bundle?.sort, entry.meta), + ) + + const bundleTypes = [ + { label: "Balanced", value: "balanced" }, + { label: "Every", value: "every" }, + ] + + return ( + +
+ v.value === item.bundle?.type) ?? { + label: item.bundle.type, + value: item.bundle.type, + }) + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}.type`, selected.value) + + if (selected.value === "balanced") { + setPath(`${pathPrefix}.${optionName}.value`, null) + } + } + }} + /> + {item.bundle?.type === "every" && ( + { + const value = parseInt(e.currentTarget.value, 10) + setPath(`${pathPrefix}.${optionName}.value`, value) + }} + defaultValue={item.bundle?.value} + /> + )} + ({ + label: entry.label, + meta: entry.meta, + value: JSON.stringify(entry.meta), + })) ?? [] + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}.sort`, selected.meta) + } + }} + /> +
+
+ ) +} + +function DiscountModeOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "discount_mode" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + return ( + + v.value === item[optionName], + )?.label ?? item[optionName], + value: item[optionName], + } + : undefined + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}`, selected.value) + } + }} + /> + + ) +} + +function ApplyOnOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "apply_on" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + return ( + + v.value === item[optionName], + )?.label ?? item[optionName], + value: item[optionName], + } + : undefined + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}`, selected.value) + } + }} + /> + {/* { + // TODO: this will be removed when we have static values for apply_on + optionRow.optionConfig?.values == null && ( + { + const suggestions = ( + await fetchCoreResourcesSuggestions( + [optionRow.mainResourceId], + `${item.selector}.${inputValue}`, + ) + ) + .filter( + (s) => + s.type === "field" && + s.value.includes(inputValue) && + s.value.endsWith("_cents"), + ) + .map((suggestion) => { + const value = suggestion.value.replace( + `${item.selector}.`, + "", + ) + + return { + value, + label: value, + } + }) + + return suggestions + }} + defaultValue={ + item[optionName] != null + ? { + label: item[optionName], + value: item[optionName], + } + : undefined + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}`, selected.value) + } + }} + /> + ) + } */} + + ) +} + +function IdentifiersOption({ + item, + pathPrefix, +}: { + item: SchemaActionItem + pathPrefix: string +}) { + const optionName = "identifiers" as const + + const { setPath } = useRuleEngine() + // const [rerenderKey, setRerenderKey] = useState(0) + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (optionRow == null || optionRow.optionConfig?.required !== true) { + return null + } + + const selectedIdentifiers = optionName in item ? item.identifiers : {} + + const allValues = optionRow.optionConfig?.values ?? [] + + return allValues.map(({ label, value }) => { + const resourceType = + value === "order.line_items.sku.id" + ? "skus" + : value === "order.line_items.bundle.id" + ? "bundles" + : value === "order.line_items.sku.sku_lists.id" + ? "sku_lists" + : undefined + if (resourceType == null) { + return null + } + return ( + + Free {label} + + } + key={value} + > + { + if (isMultiValueSelected(selected)) { + if (selected.length > 0) { + setPath(`${pathPrefix}.${optionName}`, { + ...selectedIdentifiers, + [value]: selected + .map((s) => s.value) + .filter((s) => s != null), + }) + } else { + const updatedIdentifiers = { ...selectedIdentifiers } + delete updatedIdentifiers[value] + setPath(`${pathPrefix}.${optionName}`, updatedIdentifiers) + } + } + }} + /> + + ) + }) +} + +const actionPaths = { + "order-rules": [ + "order", + "order.line_items", + "order.line_items.line_item_options", + "order.line_items.sku", + "order.line_items.bundle", + "order.line_items.shipment", + "order.line_items.payment_method", + "order.line_items.adjustment", + "order.line_items.gift_card", + ] as const, + "price-rules": ["price"] as const, +} satisfies Record diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Options/ConditionOptions.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Options/ConditionOptions.tsx new file mode 100644 index 000000000..58adec21c --- /dev/null +++ b/packages/app-elements/src/ui/forms/RuleEngine/Options/ConditionOptions.tsx @@ -0,0 +1,200 @@ +import { useState } from "react" +import { Button } from "#ui/atoms/Button" +import { Icon } from "#ui/atoms/Icon" +import { Dropdown, DropdownDivider, DropdownItem } from "#ui/composite/Dropdown" +import { InputSelect, isSingleValueSelected } from "#ui/forms/InputSelect" +import { useAvailableGroups } from "../Condition/hooks" +import { useRuleEngine } from "../RuleEngineContext" +import type { SchemaConditionItem } from "../utils" +import { AggregationRow, useOptionRow } from "./common" + +export function ConditionOptions({ + item, + pathPrefix, +}: { + item: SchemaConditionItem | null + pathPrefix: string +}) { + if (item == null) { + return null + } + + return ( + <> + + + + + ) +} + +function GroupOption({ + item, + pathPrefix, +}: { + item: SchemaConditionItem + pathPrefix: string +}) { + const optionName = "group" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + const availableGroups = useAvailableGroups() + + if (!(optionName in item) || optionRow == null || item.group === undefined) { + return null + } + + return ( + + ({ + value: group, + label: group, + }))} + defaultValue={ + item.group != null + ? { + value: item.group, + label: item.group, + } + : undefined + } + onSelect={(selected) => { + if (selected == null || isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.group`, selected?.value.toString()) + } + }} + placeholder="Select or create group…" + /> + + ) +} + +function AggregationsOption({ + item, + pathPrefix, +}: { + item: SchemaConditionItem + pathPrefix: string +}) { + const optionName = "aggregations" as const + + const { setPath } = useRuleEngine() + const [rerenderKey, setRerenderKey] = useState(0) + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if ( + !(optionName in item) || + optionRow == null || + item[optionName] == null || + item[optionName].length === 0 + ) { + return null + } + + return ( + + {item.aggregations?.map((aggregation, index) => { + const key = index.toString() + return ( +
+ + + + + } + dropdownItems={[ + { + setPath( + `${pathPrefix}.aggregations.${item.aggregations?.length}`, + {}, + ) + }} + />, + { + setPath( + `${pathPrefix}.aggregations.${item.aggregations?.length}`, + aggregation, + ) + }} + />, + , + { + if (item.aggregations?.length === 1) { + setPath(`${pathPrefix}.aggregations`, null) + } else { + setPath(`${pathPrefix}.aggregations.${index}`, null) + } + setRerenderKey((prev) => prev + 1) + }} + />, + ]} + /> +
+ ) + })} +
+ ) +} + +function ScopeOption({ + item, + pathPrefix, +}: { + item: SchemaConditionItem + pathPrefix: string +}) { + const optionName = "scope" as const + + const { setPath } = useRuleEngine() + const optionRow = useOptionRow({ item, optionName, pathPrefix }) + + if (!(optionName in item) || optionRow == null) { + return null + } + + return ( + + v.value === item[optionName], + )?.label ?? item[optionName], + value: item[optionName], + } + : undefined + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.${optionName}`, selected.value) + } + }} + /> + + ) +} diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Options/common.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Options/common.tsx new file mode 100644 index 000000000..7cbe19953 --- /dev/null +++ b/packages/app-elements/src/ui/forms/RuleEngine/Options/common.tsx @@ -0,0 +1,222 @@ +import { isEqual } from "lodash-es" +import { useCallback } from "react" +import { Icon } from "#ui/atoms/Icon" +import { Text } from "#ui/atoms/Text" +import { Dropdown, DropdownItem } from "#ui/composite/Dropdown" +import { Input } from "#ui/forms/Input" +import { InputSelect, isSingleValueSelected } from "#ui/forms/InputSelect" +import { OptionRow } from "../layout/OptionRow" +import { + type ManagedActionOption, + type ManagedConditionOption, + OPTION_LABELS, + type OptionConfig, +} from "../optionsConfig" +import { useRuleEngine } from "../RuleEngineContext" +import type { SchemaActionItem, SchemaConditionItem } from "../utils" + +export function useOptionRow({ + item, + optionName, + pathPrefix, +}: { + item: SchemaActionItem | SchemaConditionItem + optionName: ManagedActionOption | ManagedConditionOption + pathPrefix: string +}): { + optionConfig?: OptionConfig + mainResourceId: "price" | "order" + OptionRow: React.FC<{ children: React.ReactNode }> +} | null { + const { setPath, optionsConfig, schemaType } = useRuleEngine() + + const mainResourceId = + schemaType === "order-rules" + ? "order" + : schemaType === "price-rules" + ? "price" + : undefined + + const selector = item != null && "selector" in item ? item.selector : "" + + const optionConfig = + "type" in item + ? optionsConfig.actions[item.type]?.[ + selector as keyof (typeof optionsConfig.actions)[typeof item.type] + ]?.find((opt) => opt.name === optionName) + : optionsConfig.conditions.find((opt) => opt.name === optionName) + + const CustomizedOptionRow = useCallback( + ({ children }: { children: React.ReactNode }) => { + const label = optionConfig?.label ?? OPTION_LABELS[optionName] + const required = optionConfig?.required === true + + if (required) { + return ( + + {label} + + } + > + {children} + + ) + } + + return ( + { + setPath(`${pathPrefix}.${optionName}`, null) + }} + label="Remove" + disabled={optionConfig?.required === true} + />, + ]} + dropdownLabel={ + + } + /> + } + > + {children} + + ) + }, + [pathPrefix, optionName, optionConfig, setPath], + ) + + if ( + (!(optionName in item) && optionConfig == null) || + // (!(optionName in item) && optionConfig?.required === false) || + mainResourceId == null + ) { + return null + } + + return { + optionConfig, + mainResourceId, + OptionRow: CustomizedOptionRow, + } +} + +export function AggregationRow({ + aggregation, + pathPrefix, + optionConfig, +}: { + aggregation: + | NonNullable< + Extract["aggregation"] + > + | NonNullable[number] + pathPrefix: string + optionConfig?: OptionConfig +}) { + const { setPath } = useRuleEngine() + + const defaultValue = optionConfig?.values?.find( + (entry) => + isEqual(aggregation.field, entry.meta?.field) && + isEqual(aggregation.operator, entry.meta?.operator), + ) + + const matchers = [ + { label: "=", value: "eq" }, + { label: ">", value: "gt" }, + { label: "<", value: "lt" }, + { label: ">=", value: "gteq" }, + { label: "<=", value: "lteq" }, + { label: "≠", value: "not_eq" }, + { label: "multiple", value: "multiple" }, + ] + + return ( +
+ ({ + label: entry.label, + meta: entry.meta, + value: JSON.stringify(entry.meta), + })) ?? [] + } + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.field`, selected.meta?.field) + setPath(`${pathPrefix}.operator`, selected.meta?.operator) + } + }} + /> + v.value === aggregation.matcher) + ?.label ?? aggregation.matcher, + value: aggregation.matcher, + } + : undefined + } + initialValues={matchers} + onSelect={(selected) => { + if (isSingleValueSelected(selected)) { + setPath(`${pathPrefix}.matcher`, selected.value) + } + }} + /> + { + const value = parseInt(e.currentTarget.value, 10) + setPath(`${pathPrefix}.value`, value) + }} + defaultValue={aggregation.value} + /> +
+ ) +} diff --git a/packages/app-elements/src/ui/forms/RuleEngine/Options/index.tsx b/packages/app-elements/src/ui/forms/RuleEngine/Options/index.tsx index f17b59dc3..2eecf1999 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/Options/index.tsx +++ b/packages/app-elements/src/ui/forms/RuleEngine/Options/index.tsx @@ -1,752 +1,2 @@ -import { isEqual } from "lodash-es" -import { useCallback, useState } from "react" -import { Button } from "#ui/atoms/Button" -import { Icon } from "#ui/atoms/Icon" -import { Text } from "#ui/atoms/Text" -import { Dropdown, DropdownDivider, DropdownItem } from "#ui/composite/Dropdown" -import { Input } from "#ui/forms/Input" -import { InputSelect, isSingleValueSelected } from "#ui/forms/InputSelect" -import { useAvailableGroups } from "../Condition/hooks" -import { OptionRow } from "../layout/OptionRow" -import { - type ManagedActionOption, - type ManagedConditionOption, - OPTION_LABELS, - type OptionConfig, -} from "../optionsConfig" -import { useRuleEngine } from "../RuleEngineContext" -import type { SchemaActionItem, SchemaConditionItem } from "../utils" - -export function Options({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem | null - pathPrefix: string -}) { - if (item == null) { - return null - } - - return ( - <> - - - - - - - - - - - ) -} - -function GroupOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "group" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - const availableGroups = useAvailableGroups() - - if (!(optionName in item) || optionRow == null || item.group === undefined) { - return null - } - - return ( - - ({ - value: group, - label: group, - }))} - defaultValue={ - item.group != null - ? { - value: item.group, - label: item.group, - } - : undefined - } - onSelect={(selected) => { - if (selected == null || isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.group`, selected?.value.toString()) - } - }} - placeholder="Select or create group…" - /> - - ) -} - -function RoundOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "round" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null) { - return null - } - - const initialValues = [ - { label: "Yes", value: true }, - { label: "No", value: false }, - ] - - const defaultValue = initialValues.find((v) => v.value === item[optionName]) - - return ( - - { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}`, selected.value) - } - }} - /> - - ) -} - -function LimitOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "limit" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null) { - return null - } - - const defaultValue = optionRow.optionConfig?.values?.find((entry) => - isEqual(item.limit?.sort, entry.meta), - ) - - return ( - -
- { - const value = parseInt(e.currentTarget.value, 10) - setPath(`${pathPrefix}.${optionName}.value`, value) - }} - defaultValue={item.limit?.value} - /> - ({ - label: entry.label, - meta: entry.meta, - value: JSON.stringify(entry.meta), - })) ?? [] - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}.sort`, selected.meta) - } - }} - /> -
-
- ) -} - -function AggregationOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "aggregation" as const - - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null || item[optionName] == null) { - return null - } - - return ( - - - - ) -} - -function AggregationRow({ - aggregation, - pathPrefix, - optionConfig, -}: { - aggregation: - | NonNullable - | NonNullable[number] - pathPrefix: string - optionConfig?: OptionConfig -}) { - const { setPath } = useRuleEngine() - - const defaultValue = optionConfig?.values?.find( - (entry) => - isEqual(aggregation.field, entry.meta?.field) && - isEqual(aggregation.operator, entry.meta?.operator), - ) - - const matchers = [ - { label: "=", value: "eq" }, - { label: ">", value: "gt" }, - { label: "<", value: "lt" }, - { label: ">=", value: "gteq" }, - { label: "<=", value: "lteq" }, - { label: "≠", value: "not_eq" }, - { label: "multiple", value: "multiple" }, - ] - - return ( -
- ({ - label: entry.label, - meta: entry.meta, - value: JSON.stringify(entry.meta), - })) ?? [] - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.field`, selected.meta?.field) - setPath(`${pathPrefix}.operator`, selected.meta?.operator) - } - }} - /> - v.value === aggregation.matcher) - ?.label ?? aggregation.matcher, - value: aggregation.matcher, - } - : undefined - } - initialValues={matchers} - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.matcher`, selected.value) - } - }} - /> - { - const value = parseInt(e.currentTarget.value, 10) - setPath(`${pathPrefix}.value`, value) - }} - defaultValue={aggregation.value} - /> -
- ) -} - -function AggregationsOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "aggregations" as const - - const { setPath } = useRuleEngine() - const [rerenderKey, setRerenderKey] = useState(0) - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if ( - !(optionName in item) || - optionRow == null || - item[optionName] == null || - item[optionName].length === 0 - ) { - return null - } - - return ( - - {item.aggregations?.map((aggregation, index) => { - const key = index.toString() - return ( -
- - - - - } - dropdownItems={[ - { - setPath( - `${pathPrefix}.aggregations.${item.aggregations?.length}`, - {}, - ) - }} - />, - { - setPath( - `${pathPrefix}.aggregations.${item.aggregations?.length}`, - aggregation, - ) - }} - />, - , - { - if (item.aggregations?.length === 1) { - setPath(`${pathPrefix}.aggregations`, null) - } else { - setPath(`${pathPrefix}.aggregations.${index}`, null) - } - setRerenderKey((prev) => prev + 1) - }} - />, - ]} - /> -
- ) - })} -
- ) -} - -function BundleOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "bundle" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null) { - return null - } - - const defaultValue = optionRow.optionConfig?.values?.find((entry) => - isEqual(item.bundle?.sort, entry.meta), - ) - - const bundleTypes = [ - { label: "Balanced", value: "balanced" }, - { label: "Every", value: "every" }, - ] - - return ( - -
- v.value === item.bundle?.type) ?? { - label: item.bundle.type, - value: item.bundle.type, - }) - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}.type`, selected.value) - - if (selected.value === "balanced") { - setPath(`${pathPrefix}.${optionName}.value`, null) - } - } - }} - /> - {item.bundle?.type === "every" && ( - { - const value = parseInt(e.currentTarget.value, 10) - setPath(`${pathPrefix}.${optionName}.value`, value) - }} - defaultValue={item.bundle?.value} - /> - )} - ({ - label: entry.label, - meta: entry.meta, - value: JSON.stringify(entry.meta), - })) ?? [] - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}.sort`, selected.meta) - } - }} - /> -
-
- ) -} - -function DiscountModeOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "discount_mode" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null) { - return null - } - - return ( - - v.value === item[optionName], - )?.label ?? item[optionName], - value: item[optionName], - } - : undefined - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}`, selected.value) - } - }} - /> - - ) -} - -function ScopeOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "scope" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null) { - return null - } - - return ( - - v.value === item[optionName], - )?.label ?? item[optionName], - value: item[optionName], - } - : undefined - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}`, selected.value) - } - }} - /> - - ) -} - -function ApplyOnOption({ - item, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - pathPrefix: string -}) { - const optionName = "apply_on" as const - - const { setPath } = useRuleEngine() - const optionRow = useOptionRow({ item, optionName, pathPrefix }) - - if (!(optionName in item) || optionRow == null) { - return null - } - - return ( - - v.value === item[optionName], - )?.label ?? item[optionName], - value: item[optionName], - } - : undefined - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}`, selected.value) - } - }} - /> - {/* { - // TODO: this will be removed when we have static values for apply_on - optionRow.optionConfig?.values == null && ( - { - const suggestions = ( - await fetchCoreResourcesSuggestions( - [optionRow.mainResourceId], - `${item.selector}.${inputValue}`, - ) - ) - .filter( - (s) => - s.type === "field" && - s.value.includes(inputValue) && - s.value.endsWith("_cents"), - ) - .map((suggestion) => { - const value = suggestion.value.replace( - `${item.selector}.`, - "", - ) - - return { - value, - label: value, - } - }) - - return suggestions - }} - defaultValue={ - item[optionName] != null - ? { - label: item[optionName], - value: item[optionName], - } - : undefined - } - onSelect={(selected) => { - if (isSingleValueSelected(selected)) { - setPath(`${pathPrefix}.${optionName}`, selected.value) - } - }} - /> - ) - } */} - - ) -} - -function useOptionRow({ - item, - optionName, - pathPrefix, -}: { - item: SchemaActionItem | SchemaConditionItem - optionName: ManagedActionOption | ManagedConditionOption - pathPrefix: string -}): { - optionConfig?: OptionConfig - mainResourceId: "price" | "order" - OptionRow: React.FC<{ children: React.ReactNode }> -} | null { - const { setPath, optionsConfig, schemaType } = useRuleEngine() - - const mainResourceId = - schemaType === "order-rules" - ? "order" - : schemaType === "price-rules" - ? "price" - : undefined - - const optionConfig = - "type" in item && item.selector != null - ? optionsConfig.actions[item.type]?.[ - item.selector as keyof (typeof optionsConfig.actions)[typeof item.type] - ]?.find((opt) => opt.name === optionName) - : optionsConfig.conditions.find((opt) => opt.name === optionName) - - const CustomizedOptionRow = useCallback( - ({ children }: { children: React.ReactNode }) => { - return ( - { - setPath(`${pathPrefix}.${optionName}`, null) - }} - label="Remove" - />, - ]} - dropdownLabel={ - - } - /> - } - > - {children} - - ) - }, - [pathPrefix, optionName, optionConfig, setPath], - ) - - if (!(optionName in item) || mainResourceId == null) { - return null - } - - return { - optionConfig, - mainResourceId, - OptionRow: CustomizedOptionRow, - } -} +export { ActionOptions } from "./ActionOptions" +export { ConditionOptions } from "./ConditionOptions" diff --git a/packages/app-elements/src/ui/forms/RuleEngine/RuleEngineContext.tsx b/packages/app-elements/src/ui/forms/RuleEngine/RuleEngineContext.tsx index 858c3e08f..d84e43146 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/RuleEngineContext.tsx +++ b/packages/app-elements/src/ui/forms/RuleEngine/RuleEngineContext.tsx @@ -74,24 +74,24 @@ const RuleEngineContext = createContext( // } // } +// // Ensure that if we are setting a field inside an action, the action has a groups array +// if (/actions\.\d\.[\w_]+$/.test(action.path)) { +// const parentPath = action.path.replace(/\.[\w_]+$/, "") +// const parentValue = get(newValue, parentPath) as Record< +// string, +// unknown +// > | null + +// if (parentValue?.groups == null) { +// set(newValue, `${parentPath}.groups`, []) +// } +// } + function ruleEngineReducer(state: State, action: Action): State { switch (action.type) { case "SET_PATH": { const newValue = cloneDeep(state.value) - // Ensure that if we are setting a field inside an action, the action has a groups array - if (/actions\.\d\.[\w_]+$/.test(action.path)) { - const parentPath = action.path.replace(/\.[\w_]+$/, "") - const parentValue = get(newValue, parentPath) as Record< - string, - unknown - > | null - - if (parentValue?.groups == null) { - set(newValue, `${parentPath}.groups`, []) - } - } - if (action.value === null && action.allowNullValue === false) { if (/\.\d+$/.test(action.path)) { // If the path ends with a number, we assume it's an array index diff --git a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.json b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.json index c0a58cbcd..9855c1e5e 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.json +++ b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "1.1.7", + "$id": "1.2.0", "title": "Rules for order context", "description": "Rules payload within order context for the rules engine of Commerce Layer.", "properties": { @@ -81,7 +81,119 @@ "apply_on": { "$ref": "#/$defs/apply_on" }, "limit": { "$ref": "#/$defs/limit" } }, - "required": ["type", "selector", "value"], + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["percentage"], + "description": "The type of action you want to apply.", + "examples": ["percentage"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "number", + "description": "Percentage to be discounted,", + "examples": [0.2, 0.3] + }, + "round": { "$ref": "#/$defs/round" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "bundle": { "$ref": "#/$defs/bundle" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["percentage"], + "description": "The type of action you want to apply.", + "examples": ["percentage"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "number", + "description": "Percentage to be discounted,", + "examples": [0.2, 0.3] + }, + "round": { "$ref": "#/$defs/round" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "limit": { "$ref": "#/$defs/limit" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_amount"], + "description": "The type of action you want to apply.", + "examples": ["fixed_amount"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The discount fixed amount to be applied.", + "examples": [10] + }, + "bundle": { "$ref": "#/$defs/bundle" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "discount_mode": { + "type": "string", + "enum": ["distributed", "default"], + "description": "The type of distribution of the discount over the items.", + "examples": ["distributed"] + } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_amount"], + "description": "The type of action you want to apply.", + "examples": ["fixed_amount"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The discount fixed amount to be applied.", + "examples": [10] + }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "limit": { "$ref": "#/$defs/limit" }, + "discount_mode": { + "type": "string", + "enum": ["distributed", "default"], + "description": "The type of distribution of the discount over the items.", + "examples": ["distributed"] + } + }, + "required": ["type", "selector", "value", "groups"], "additionalProperties": false }, { @@ -104,6 +216,7 @@ }, "bundle": { "$ref": "#/$defs/bundle" }, "apply_on": { "$ref": "#/$defs/apply_on" }, + "quantity": { "$ref": "#/$defs/quantity" }, "discount_mode": { "type": "string", "enum": ["distributed", "default"], @@ -134,6 +247,7 @@ }, "apply_on": { "$ref": "#/$defs/apply_on" }, "limit": { "$ref": "#/$defs/limit" }, + "quantity": { "$ref": "#/$defs/quantity" }, "discount_mode": { "type": "string", "enum": ["distributed", "default"], @@ -141,7 +255,7 @@ "examples": ["distributed"] } }, - "required": ["type", "selector", "value"], + "required": ["type", "selector", "value", "groups"], "additionalProperties": false }, { @@ -189,7 +303,57 @@ "apply_on": { "$ref": "#/$defs/apply_on" }, "limit": { "$ref": "#/$defs/limit" } }, - "required": ["type", "selector", "value"], + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_price"], + "description": "The type of action you want to apply.", + "examples": ["fixed_price"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The price fixed amount to be applied.", + "examples": [20] + }, + "bundle": { "$ref": "#/$defs/bundle" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_price"], + "description": "The type of action you want to apply.", + "examples": ["fixed_price"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The price fixed amount to be applied.", + "examples": [20] + }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "limit": { "$ref": "#/$defs/limit" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], "additionalProperties": false }, { @@ -226,7 +390,7 @@ "required": ["x", "y"] } }, - "required": ["type", "value"], + "required": ["type", "value", "groups", "selector"], "additionalProperties": false }, { @@ -267,7 +431,42 @@ "required": ["x", "y", "attribute"] } }, - "required": ["type", "value", "selector"], + "required": ["type", "value", "selector", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["free_gift"], + "description": "The type of action you want to apply.", + "examples": ["free_gift"] + }, + "identifiers": { + "type": "object", + "description": "Object whose keys are allowed selector paths (order.line_items.sku.id, order.line_items.bundle.id, order.line_items.sku.sku_lists.id). Each value is an array of ids to match. Line items matching any (selector, id) are eligible for the free gift.", + "additionalProperties": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "propertyNames": { + "enum": [ + "order.line_items.sku.id", + "order.line_items.bundle.id", + "order.line_items.sku.sku_lists.id" + ] + }, + "minProperties": 1 + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "Total number of units to discount across all matching line items. Distributed in priority order (keys, then ids, then line item order). Required." + } + }, + "required": ["type", "identifiers", "quantity"], "additionalProperties": false } ] @@ -468,6 +667,36 @@ ], "format": "date-time" }, + { + "type": "string", + "description": "Value to compare against the field value.", + "examples": [ + "{\"matcher\": \"eq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"not_eq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"lt\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"lteq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"gt\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"gteq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}" + ], + "matcherTypes": [ + "eq", + "not_eq", + "lt", + "lteq", + "gt", + "gteq" + ], + "format": "date-time-dynamic" + }, + { + "type": "string", + "description": "Value to compare against the field value.", + "examples": [ + "{\"matcher\": \"gteq_lteq\", \"value\": \"{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 7.days)}}\"}\"}" + ], + "matcherTypes": ["gteq_lteq"], + "format": "date-time-range" + }, { "type": "boolean", "description": "Value to compare against the field value.", @@ -528,6 +757,36 @@ "is_in", "is_not_in" ] + }, + { + "type": "string", + "format": "date-time-dynamic", + "examples": [ + "{\"matcher\": \"eq\", \"value\": \"{{today}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{day(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{year(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{order.updated_at}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(order.updated_at)}}\"}" + ], + "matcherTypes": [ + "eq", + "not_eq", + "lt", + "lteq", + "gt", + "gteq" + ] + }, + { + "type": "string", + "format": "date-time-range", + "examples": [ + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 7.days)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 2.months)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 1.years)}}\"}" + ], + "matcherTypes": ["gteq_lteq"] } ] }, @@ -545,7 +804,16 @@ "{\"matcher\": \"gteq_lteq\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", "{\"matcher\": \"gt_lteq\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", "{\"matcher\": \"gteq_lt\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", - "{\"matcher\": \"gt_lt\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}" + "{\"matcher\": \"gt_lt\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", + "{\"matcher\": \"eq\", \"value\": \"{{today}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{day(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{year(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{order.updated_at}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(order.updated_at)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 7.days)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 2.months)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 1.years)}}\"}" ], "matcherTypes": [ "gt_lt", @@ -611,7 +879,20 @@ } }, "required": ["field", "value", "matcher"], - "additionalProperties": false + "additionalProperties": false, + "if": { + "properties": { + "value": { + "type": "string", + "pattern": "\\{\\{range\\s*\\(.+\\)\\}\\}" + } + }, + "required": ["value"] + }, + "then": { + "properties": { "matcher": { "const": "gteq_lteq" } }, + "required": ["matcher"] + } }, { "type": "object", @@ -666,7 +947,8 @@ "required": ["field", "matcher"], "additionalProperties": false } - ] + ], + "minItems": 1 } }, "conditions_logic": { @@ -779,6 +1061,11 @@ "description": "If true, rounds the discount, only available on percentage actions.", "examples": [true, false] }, + "quantity": { + "type": "integer", + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "examples": [1, 2] + }, "aggregation": { "type": "object", "properties": { diff --git a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.schema.ts b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.schema.ts index 4a360b801..c9d1bae33 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.schema.ts +++ b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/order_rules.schema.ts @@ -155,6 +155,10 @@ export type Bundle = */ value: number } +/** + * Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity. + */ +export type Quantity = number /** * Rules payload within order context for the rules engine of Commerce Layer. @@ -206,7 +210,24 @@ export interface RulesForOrderContext { type: "percentage" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups + aggregation?: Aggregation + /** + * Percentage to be discounted, + */ + value: number + round?: Round + apply_on?: ApplyOn + limit?: Limit + } + | { + /** + * The type of action you want to apply. + */ + type: "percentage" + selector: Selector + identifier?: Identifier + groups: Groups aggregation?: Aggregation /** * Percentage to be discounted, @@ -214,7 +235,66 @@ export interface RulesForOrderContext { value: number round?: Round apply_on?: ApplyOn + bundle?: Bundle + quantity?: Quantity + } + | { + /** + * The type of action you want to apply. + */ + type: "percentage" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * Percentage to be discounted, + */ + value: number + round?: Round + apply_on?: ApplyOn + limit?: Limit + quantity?: Quantity + } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_amount" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The discount fixed amount to be applied. + */ + value: number + bundle?: Bundle + apply_on?: ApplyOn + /** + * The type of distribution of the discount over the items. + */ + discount_mode?: "distributed" | "default" + } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_amount" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The discount fixed amount to be applied. + */ + value: number + apply_on?: ApplyOn limit?: Limit + /** + * The type of distribution of the discount over the items. + */ + discount_mode?: "distributed" | "default" } | { /** @@ -231,6 +311,7 @@ export interface RulesForOrderContext { value: number bundle?: Bundle apply_on?: ApplyOn + quantity?: Quantity /** * The type of distribution of the discount over the items. */ @@ -243,7 +324,7 @@ export interface RulesForOrderContext { type: "fixed_amount" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups aggregation?: Aggregation /** * The discount fixed amount to be applied. @@ -251,6 +332,7 @@ export interface RulesForOrderContext { value: number apply_on?: ApplyOn limit?: Limit + quantity?: Quantity /** * The type of distribution of the discount over the items. */ @@ -279,7 +361,7 @@ export interface RulesForOrderContext { type: "fixed_price" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups aggregation?: Aggregation /** * The price fixed amount to be applied. @@ -288,14 +370,48 @@ export interface RulesForOrderContext { apply_on?: ApplyOn limit?: Limit } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_price" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The price fixed amount to be applied. + */ + value: number + bundle?: Bundle + apply_on?: ApplyOn + quantity?: Quantity + } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_price" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The price fixed amount to be applied. + */ + value: number + apply_on?: ApplyOn + limit?: Limit + quantity?: Quantity + } | { /** * The type of action you want to apply. */ type: "buy_x_pay_y" - selector?: Selector + selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups aggregation?: Aggregation value: { /** @@ -320,7 +436,7 @@ export interface RulesForOrderContext { type: "every_x_discount_y" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups aggregation?: Aggregation value: { /** @@ -338,6 +454,25 @@ export interface RulesForOrderContext { [k: string]: unknown } } + | { + /** + * The type of action you want to apply. + */ + type: "free_gift" + /** + * Object whose keys are allowed selector paths (order.line_items.sku.id, order.line_items.bundle.id, order.line_items.sku.sku_lists.id). Each value is an array of ids to match. Line items matching any (selector, id) are eligible for the free gift. + */ + identifiers: { + /** + * @minItems 1 + */ + [k: string]: [string, ...string[]] + } + /** + * Total number of units to discount across all matching line items. Distributed in priority order (keys, then ids, then line item order). Required. + */ + quantity: number + } )[] }[] [k: string]: unknown diff --git a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/organization_config.json b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/organization_config.json index c001a0e3f..b377f407e 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/organization_config.json +++ b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/organization_config.json @@ -71,6 +71,25 @@ "description": "Where streaming is enabled: none, live, test or both." } } + }, + "anomalies": { + "type": "object", + "additionalProperties": false, + "description": "Configs for anomalies alerts", + "properties": { + "orders": { + "type": "object", + "description": "Configs for anomalies alerts on orders.", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enables anomaly alerts on orders." + }, + "recipients": { "$ref": "#/$defs/recipients" } + } + } + } } } } @@ -226,6 +245,10 @@ "required": ["value", "label"], "additionalProperties": false } + }, + "recipients": { + "type": "string", + "description": "Comma separated list of email recipients for the notifications (ex: test0@commercelayer.io,test1@sommercelayer.io)." } } } diff --git a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.json b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.json index ff07dcfc9..2bf60d8f9 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.json +++ b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "1.1.7", + "$id": "1.2.0", "title": "Rules for price context", "description": "Rules payload within price context for the rules engine of Commerce Layer.", "properties": { @@ -81,7 +81,59 @@ "apply_on": { "$ref": "#/$defs/apply_on" }, "limit": { "$ref": "#/$defs/limit" } }, - "required": ["type", "selector", "value"], + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["percentage"], + "description": "The type of action you want to apply.", + "examples": ["percentage"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "number", + "description": "Percentage to be discounted,", + "examples": [0.2, 0.3] + }, + "round": { "$ref": "#/$defs/round" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "bundle": { "$ref": "#/$defs/bundle" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["percentage"], + "description": "The type of action you want to apply.", + "examples": ["percentage"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "number", + "description": "Percentage to be discounted,", + "examples": [0.2, 0.3] + }, + "round": { "$ref": "#/$defs/round" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "limit": { "$ref": "#/$defs/limit" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], "additionalProperties": false }, { @@ -141,7 +193,69 @@ "examples": ["distributed"] } }, - "required": ["type", "selector", "value"], + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_amount"], + "description": "The type of action you want to apply.", + "examples": ["fixed_amount"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The discount fixed amount to be applied.", + "examples": [10] + }, + "bundle": { "$ref": "#/$defs/bundle" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "quantity": { "$ref": "#/$defs/quantity" }, + "discount_mode": { + "type": "string", + "enum": ["distributed", "default"], + "description": "The type of distribution of the discount over the items.", + "examples": ["distributed"] + } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_amount"], + "description": "The type of action you want to apply.", + "examples": ["fixed_amount"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The discount fixed amount to be applied.", + "examples": [10] + }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "limit": { "$ref": "#/$defs/limit" }, + "quantity": { "$ref": "#/$defs/quantity" }, + "discount_mode": { + "type": "string", + "enum": ["distributed", "default"], + "description": "The type of distribution of the discount over the items.", + "examples": ["distributed"] + } + }, + "required": ["type", "selector", "value", "groups"], "additionalProperties": false }, { @@ -189,7 +303,57 @@ "apply_on": { "$ref": "#/$defs/apply_on" }, "limit": { "$ref": "#/$defs/limit" } }, - "required": ["type", "selector", "value"], + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_price"], + "description": "The type of action you want to apply.", + "examples": ["fixed_price"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The price fixed amount to be applied.", + "examples": [20] + }, + "bundle": { "$ref": "#/$defs/bundle" }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["fixed_price"], + "description": "The type of action you want to apply.", + "examples": ["fixed_price"] + }, + "selector": { "$ref": "#/$defs/selector" }, + "identifier": { "$ref": "#/$defs/identifier" }, + "groups": { "$ref": "#/$defs/groups" }, + "aggregation": { "$ref": "#/$defs/aggregation" }, + "value": { + "type": "integer", + "description": "The price fixed amount to be applied.", + "examples": [20] + }, + "apply_on": { "$ref": "#/$defs/apply_on" }, + "limit": { "$ref": "#/$defs/limit" }, + "quantity": { "$ref": "#/$defs/quantity" } + }, + "required": ["type", "selector", "value", "groups"], "additionalProperties": false } ] @@ -390,6 +554,36 @@ ], "format": "date-time" }, + { + "type": "string", + "description": "Value to compare against the field value.", + "examples": [ + "{\"matcher\": \"eq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"not_eq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"lt\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"lteq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"gt\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}", + "{\"matcher\": \"gteq\", \"value\": \"{\"matcher\": \"eq\", \"value\": \"{{today}}\"}\"}" + ], + "matcherTypes": [ + "eq", + "not_eq", + "lt", + "lteq", + "gt", + "gteq" + ], + "format": "date-time-dynamic" + }, + { + "type": "string", + "description": "Value to compare against the field value.", + "examples": [ + "{\"matcher\": \"gteq_lteq\", \"value\": \"{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 7.days)}}\"}\"}" + ], + "matcherTypes": ["gteq_lteq"], + "format": "date-time-range" + }, { "type": "boolean", "description": "Value to compare against the field value.", @@ -450,6 +644,36 @@ "is_in", "is_not_in" ] + }, + { + "type": "string", + "format": "date-time-dynamic", + "examples": [ + "{\"matcher\": \"eq\", \"value\": \"{{today}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{day(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{year(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{order.updated_at}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(order.updated_at)}}\"}" + ], + "matcherTypes": [ + "eq", + "not_eq", + "lt", + "lteq", + "gt", + "gteq" + ] + }, + { + "type": "string", + "format": "date-time-range", + "examples": [ + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 7.days)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 2.months)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 1.years)}}\"}" + ], + "matcherTypes": ["gteq_lteq"] } ] }, @@ -467,7 +691,16 @@ "{\"matcher\": \"gteq_lteq\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", "{\"matcher\": \"gt_lteq\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", "{\"matcher\": \"gteq_lt\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", - "{\"matcher\": \"gt_lt\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}" + "{\"matcher\": \"gt_lt\", \"value\": [\"2024-09-25T23:34:49Z\", \"2024-09-26T23:34:49Z\"]}", + "{\"matcher\": \"eq\", \"value\": \"{{today}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{day(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{year(today)}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{order.updated_at}}\"}", + "{\"matcher\": \"eq\", \"value\": \"{{month(order.updated_at)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 7.days)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 2.months)}}\"}", + "{\"matcher\": \"gteq_lteq\", \"value\": \"{{range(today, 1.years)}}\"}" ], "matcherTypes": [ "gt_lt", @@ -533,7 +766,20 @@ } }, "required": ["field", "value", "matcher"], - "additionalProperties": false + "additionalProperties": false, + "if": { + "properties": { + "value": { + "type": "string", + "pattern": "\\{\\{range\\s*\\(.+\\)\\}\\}" + } + }, + "required": ["value"] + }, + "then": { + "properties": { "matcher": { "const": "gteq_lteq" } }, + "required": ["matcher"] + } }, { "type": "object", @@ -588,7 +834,8 @@ "required": ["field", "matcher"], "additionalProperties": false } - ] + ], + "minItems": 1 } }, "conditions_logic": { @@ -701,6 +948,11 @@ "description": "If true, rounds the discount, only available on percentage actions.", "examples": [true, false] }, + "quantity": { + "type": "integer", + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "examples": [1, 2] + }, "aggregation": { "type": "object", "properties": { diff --git a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.schema.ts b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.schema.ts index 1abadb3bf..271cba975 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.schema.ts +++ b/packages/app-elements/src/ui/forms/RuleEngine/json_schema/price_rules.schema.ts @@ -155,6 +155,10 @@ export type Bundle = */ value: number } +/** + * Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity. + */ +export type Quantity = number /** * Rules payload within price context for the rules engine of Commerce Layer. @@ -206,7 +210,42 @@ export interface RulesForPriceContext { type: "percentage" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups + aggregation?: Aggregation + /** + * Percentage to be discounted, + */ + value: number + round?: Round + apply_on?: ApplyOn + limit?: Limit + } + | { + /** + * The type of action you want to apply. + */ + type: "percentage" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * Percentage to be discounted, + */ + value: number + round?: Round + apply_on?: ApplyOn + bundle?: Bundle + quantity?: Quantity + } + | { + /** + * The type of action you want to apply. + */ + type: "percentage" + selector: Selector + identifier?: Identifier + groups: Groups aggregation?: Aggregation /** * Percentage to be discounted, @@ -215,6 +254,7 @@ export interface RulesForPriceContext { round?: Round apply_on?: ApplyOn limit?: Limit + quantity?: Quantity } | { /** @@ -243,7 +283,7 @@ export interface RulesForPriceContext { type: "fixed_amount" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups aggregation?: Aggregation /** * The discount fixed amount to be applied. @@ -256,6 +296,48 @@ export interface RulesForPriceContext { */ discount_mode?: "distributed" | "default" } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_amount" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The discount fixed amount to be applied. + */ + value: number + bundle?: Bundle + apply_on?: ApplyOn + quantity?: Quantity + /** + * The type of distribution of the discount over the items. + */ + discount_mode?: "distributed" | "default" + } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_amount" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The discount fixed amount to be applied. + */ + value: number + apply_on?: ApplyOn + limit?: Limit + quantity?: Quantity + /** + * The type of distribution of the discount over the items. + */ + discount_mode?: "distributed" | "default" + } | { /** * The type of action you want to apply. @@ -279,7 +361,40 @@ export interface RulesForPriceContext { type: "fixed_price" selector: Selector identifier?: Identifier - groups?: Groups + groups: Groups + aggregation?: Aggregation + /** + * The price fixed amount to be applied. + */ + value: number + apply_on?: ApplyOn + limit?: Limit + } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_price" + selector: Selector + identifier?: Identifier + groups: Groups + aggregation?: Aggregation + /** + * The price fixed amount to be applied. + */ + value: number + bundle?: Bundle + apply_on?: ApplyOn + quantity?: Quantity + } + | { + /** + * The type of action you want to apply. + */ + type: "fixed_price" + selector: Selector + identifier?: Identifier + groups: Groups aggregation?: Aggregation /** * The price fixed amount to be applied. @@ -287,6 +402,7 @@ export interface RulesForPriceContext { value: number apply_on?: ApplyOn limit?: Limit + quantity?: Quantity } )[] }[] diff --git a/packages/app-elements/src/ui/forms/RuleEngine/layout/OptionRow.tsx b/packages/app-elements/src/ui/forms/RuleEngine/layout/OptionRow.tsx index d2c1e079b..73c5a7125 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/layout/OptionRow.tsx +++ b/packages/app-elements/src/ui/forms/RuleEngine/layout/OptionRow.tsx @@ -8,9 +8,7 @@ export const OptionRow: React.FC<{ }> = ({ children, label, className }) => { return (
-
- {label} -
+
{label}
{children}
) diff --git a/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.test.ts b/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.test.ts index edeb79bff..57756b3e1 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.test.ts +++ b/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.test.ts @@ -14,29 +14,431 @@ describe("parseOptionsFromSchema", () => { { "actions": { "buy_x_pay_y": { - "order": [], - "order.line_items": [], - "order.line_items.adjustment": [], - "order.line_items.bundle": [], - "order.line_items.gift_card": [], - "order.line_items.line_item_options": [], - "order.line_items.payment_method": [], - "order.line_items.shipment": [], - "order.line_items.sku": [], + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + ], + "order": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.adjustment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.bundle": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.gift_card": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.line_item_options": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.payment_method": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.shipment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.sku": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], }, "every_x_discount_y": { - "order": [], - "order.line_items": [], - "order.line_items.adjustment": [], - "order.line_items.bundle": [], - "order.line_items.gift_card": [], - "order.line_items.line_item_options": [], - "order.line_items.payment_method": [], - "order.line_items.shipment": [], - "order.line_items.sku": [], + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + ], + "order": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.adjustment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.bundle": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.gift_card": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.line_item_options": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.payment_method": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.shipment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.sku": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], }, "fixed_amount": { + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, + ], "order": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "Creates bundles based on the groups provided.", "label": "Bundle", @@ -44,6 +446,7 @@ describe("parseOptionsFromSchema", () => { "limit", ], "name": "bundle", + "required": false, "valueType": "object", "values": [ { @@ -69,6 +472,7 @@ describe("parseOptionsFromSchema", () => { "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -86,6 +490,7 @@ describe("parseOptionsFromSchema", () => { "label": "Discount mode", "mutuallyExclusiveWith": [], "name": "discount_mode", + "required": false, "valueType": "string", "values": [ { @@ -105,6 +510,7 @@ describe("parseOptionsFromSchema", () => { "bundle", ], "name": "limit", + "required": false, "valueType": "object", "values": [ { @@ -125,8 +531,35 @@ describe("parseOptionsFromSchema", () => { }, ], }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, ], "order.line_items": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "Creates bundles based on the groups provided.", "label": "Bundle", @@ -134,6 +567,7 @@ describe("parseOptionsFromSchema", () => { "limit", ], "name": "bundle", + "required": false, "valueType": "object", "values": [ { @@ -159,6 +593,7 @@ describe("parseOptionsFromSchema", () => { "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -176,6 +611,7 @@ describe("parseOptionsFromSchema", () => { "label": "Discount mode", "mutuallyExclusiveWith": [], "name": "discount_mode", + "required": false, "valueType": "string", "values": [ { @@ -195,6 +631,7 @@ describe("parseOptionsFromSchema", () => { "bundle", ], "name": "limit", + "required": false, "valueType": "object", "values": [ { @@ -215,17 +652,197 @@ describe("parseOptionsFromSchema", () => { }, ], }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, ], - "order.line_items.adjustment": [], - "order.line_items.bundle": [], - "order.line_items.gift_card": [], - "order.line_items.line_item_options": [], - "order.line_items.payment_method": [], - "order.line_items.shipment": [], - "order.line_items.sku": [], - }, - "fixed_price": { - "order": [ + "order.line_items.adjustment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.bundle": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.gift_card": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.line_item_options": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.payment_method": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.shipment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.sku": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + }, + "fixed_price": { + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, + ], + "order": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "Creates bundles based on the groups provided.", "label": "Bundle", @@ -233,6 +850,7 @@ describe("parseOptionsFromSchema", () => { "limit", ], "name": "bundle", + "required": false, "valueType": "object", "values": [ { @@ -258,6 +876,7 @@ describe("parseOptionsFromSchema", () => { "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -271,104 +890,363 @@ describe("parseOptionsFromSchema", () => { ], }, { - "description": "Restriction on how many resources will be affected by the action.", - "label": "Limit", - "mutuallyExclusiveWith": [ - "bundle", - ], - "name": "limit", + "description": "Restriction on how many resources will be affected by the action.", + "label": "Limit", + "mutuallyExclusiveWith": [ + "bundle", + ], + "name": "limit", + "required": false, + "valueType": "object", + "values": [ + { + "label": "Most expensive", + "meta": { + "attribute": "total_amount_cents", + "direction": "desc", + }, + "value": "most-expensive", + }, + { + "label": "Less expensive", + "meta": { + "attribute": "total_amount_cents", + "direction": "asc", + }, + "value": "less-expensive", + }, + ], + }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, + ], + "order.line_items": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + { + "description": "Creates bundles based on the groups provided.", + "label": "Bundle", + "mutuallyExclusiveWith": [ + "limit", + ], + "name": "bundle", + "required": false, + "valueType": "object", + "values": [ + { + "label": "Most expensive", + "meta": { + "attribute": "unit_amount_cents", + "direction": "desc", + }, + "value": "most-expensive", + }, + { + "label": "Less expensive", + "meta": { + "attribute": "unit_amount_cents", + "direction": "asc", + }, + "value": "less-expensive", + }, + ], + }, + { + "description": "If provided, applies the action to a specific attribute instead of the default one.", + "label": "Apply on", + "mutuallyExclusiveWith": [], + "name": "apply_on", + "required": false, + "valueType": "string", + "values": [ + { + "label": "Unit amount", + "value": "unit_amount_cents", + }, + { + "label": "Compare at amount", + "value": "compare_at_amount_cents", + }, + ], + }, + { + "description": "Restriction on how many resources will be affected by the action.", + "label": "Limit", + "mutuallyExclusiveWith": [ + "bundle", + ], + "name": "limit", + "required": false, + "valueType": "object", + "values": [ + { + "label": "Most expensive", + "meta": { + "attribute": "unit_amount_cents", + "direction": "desc", + }, + "value": "most-expensive", + }, + { + "label": "Less expensive", + "meta": { + "attribute": "unit_amount_cents", + "direction": "asc", + }, + "value": "less-expensive", + }, + ], + }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, + ], + "order.line_items.adjustment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.bundle": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.gift_card": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.line_item_options": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.payment_method": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.shipment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.sku": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + }, + "free_gift": { + "": [ + { + "description": "Object whose keys are allowed selector paths (order.line_items.sku.id, order.line_items.bundle.id, order.line_items.sku.sku_lists.id). Each value is an array of ids to match. Line items matching any (selector, id) are eligible for the free gift.", + "label": "Identifiers", + "mutuallyExclusiveWith": [], + "name": "identifiers", + "required": true, "valueType": "object", "values": [ { - "label": "Most expensive", - "meta": { - "attribute": "total_amount_cents", - "direction": "desc", - }, - "value": "most-expensive", + "label": "SKU", + "value": "order.line_items.sku.id", }, { - "label": "Less expensive", - "meta": { - "attribute": "total_amount_cents", - "direction": "asc", - }, - "value": "less-expensive", + "label": "Bundle", + "value": "order.line_items.bundle.id", + }, + { + "label": "SKU list", + "value": "order.line_items.sku.sku_lists.id", }, ], }, + { + "description": "Total number of units to discount across all matching line items. Distributed in priority order (keys, then ids, then line item order). Required.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": true, + "valueType": "integer", + "values": undefined, + }, ], - "order.line_items": [ + "order": [ { - "description": "Creates bundles based on the groups provided.", - "label": "Bundle", - "mutuallyExclusiveWith": [ - "limit", - ], - "name": "bundle", + "description": "Object whose keys are allowed selector paths (order.line_items.sku.id, order.line_items.bundle.id, order.line_items.sku.sku_lists.id). Each value is an array of ids to match. Line items matching any (selector, id) are eligible for the free gift.", + "label": "Identifiers", + "mutuallyExclusiveWith": [], + "name": "identifiers", + "required": true, "valueType": "object", "values": [ { - "label": "Most expensive", - "meta": { - "attribute": "unit_amount_cents", - "direction": "desc", - }, - "value": "most-expensive", + "label": "SKU", + "value": "order.line_items.sku.id", }, { - "label": "Less expensive", - "meta": { - "attribute": "unit_amount_cents", - "direction": "asc", - }, - "value": "less-expensive", + "label": "Bundle", + "value": "order.line_items.bundle.id", + }, + { + "label": "SKU list", + "value": "order.line_items.sku.sku_lists.id", }, ], }, { - "description": "If provided, applies the action to a specific attribute instead of the default one.", - "label": "Apply on", + "description": "Total number of units to discount across all matching line items. Distributed in priority order (keys, then ids, then line item order). Required.", + "label": "Quantity", "mutuallyExclusiveWith": [], - "name": "apply_on", - "valueType": "string", - "values": [ - { - "label": "Unit amount", - "value": "unit_amount_cents", - }, - { - "label": "Compare at amount", - "value": "compare_at_amount_cents", - }, - ], + "name": "quantity", + "required": true, + "valueType": "integer", + "values": undefined, }, + ], + "order.line_items": [ { - "description": "Restriction on how many resources will be affected by the action.", - "label": "Limit", - "mutuallyExclusiveWith": [ - "bundle", - ], - "name": "limit", - "valueType": "object", - "values": [ - { - "label": "Most expensive", - "meta": { - "attribute": "unit_amount_cents", - "direction": "desc", - }, - "value": "most-expensive", - }, - { - "label": "Less expensive", - "meta": { - "attribute": "unit_amount_cents", - "direction": "asc", - }, - "value": "less-expensive", - }, - ], + "description": "Total number of units to discount across all matching line items. Distributed in priority order (keys, then ids, then line item order). Required.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": true, + "valueType": "integer", + "values": undefined, }, ], "order.line_items.adjustment": [], @@ -380,12 +1258,51 @@ describe("parseOptionsFromSchema", () => { "order.line_items.sku": [], }, "percentage": { + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, + ], "order": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "If true, rounds the discount, only available on percentage actions.", "label": "Round", "mutuallyExclusiveWith": [], "name": "round", + "required": false, "valueType": "boolean", "values": undefined, }, @@ -394,6 +1311,7 @@ describe("parseOptionsFromSchema", () => { "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -413,6 +1331,7 @@ describe("parseOptionsFromSchema", () => { "limit", ], "name": "bundle", + "required": false, "valueType": "object", "values": [ { @@ -440,6 +1359,7 @@ describe("parseOptionsFromSchema", () => { "bundle", ], "name": "limit", + "required": false, "valueType": "object", "values": [ { @@ -460,13 +1380,41 @@ describe("parseOptionsFromSchema", () => { }, ], }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, ], "order.line_items": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "If true, rounds the discount, only available on percentage actions.", "label": "Round", "mutuallyExclusiveWith": [], "name": "round", + "required": false, "valueType": "boolean", "values": undefined, }, @@ -475,6 +1423,7 @@ describe("parseOptionsFromSchema", () => { "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -494,6 +1443,7 @@ describe("parseOptionsFromSchema", () => { "limit", ], "name": "bundle", + "required": false, "valueType": "object", "values": [ { @@ -521,6 +1471,7 @@ describe("parseOptionsFromSchema", () => { "bundle", ], "name": "limit", + "required": false, "valueType": "object", "values": [ { @@ -541,14 +1492,156 @@ describe("parseOptionsFromSchema", () => { }, ], }, + { + "description": "Optional quantity to override the resource quantity. If specified, the action will apply to the minimum of specified quantity and resource quantity.", + "label": "Quantity", + "mutuallyExclusiveWith": [], + "name": "quantity", + "required": false, + "valueType": "integer", + "values": undefined, + }, + ], + "order.line_items.adjustment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.bundle": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.gift_card": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.line_item_options": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.payment_method": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.shipment": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, + ], + "order.line_items.sku": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, ], - "order.line_items.adjustment": [], - "order.line_items.bundle": [], - "order.line_items.gift_card": [], - "order.line_items.line_item_options": [], - "order.line_items.payment_method": [], - "order.line_items.shipment": [], - "order.line_items.sku": [], }, }, "conditions": [ @@ -557,6 +1650,7 @@ describe("parseOptionsFromSchema", () => { "label": "Scope", "mutuallyExclusiveWith": [], "name": "scope", + "required": false, "valueType": "string", "values": [ { @@ -574,6 +1668,7 @@ describe("parseOptionsFromSchema", () => { "label": "Group", "mutuallyExclusiveWith": [], "name": "group", + "required": false, "valueType": "string", "values": undefined, }, @@ -582,6 +1677,7 @@ describe("parseOptionsFromSchema", () => { "label": "Aggregations", "mutuallyExclusiveWith": [], "name": "aggregations", + "required": false, "valueType": "array", "values": [ { @@ -617,12 +1713,42 @@ describe("parseOptionsFromSchema", () => { { "actions": { "fixed_amount": { + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + ], "price": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "If provided, applies the action to a specific attribute instead of the default one.", "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -638,12 +1764,42 @@ describe("parseOptionsFromSchema", () => { ], }, "fixed_price": { + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + ], "price": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "If provided, applies the action to a specific attribute instead of the default one.", "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -659,12 +1815,42 @@ describe("parseOptionsFromSchema", () => { ], }, "percentage": { + "": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + ], "price": [ + { + "description": "The resource on which to apply the action (expressed in dot notation). Can be an attribute if you set also the identifier key.", + "label": "Apply to", + "mutuallyExclusiveWith": [], + "name": "selector", + "required": true, + "valueType": "string", + "values": undefined, + }, + { + "description": "The groups on which to apply the action (must be one or more among the ones defined when grouping the matches of the related conditions).", + "label": "Groups", + "mutuallyExclusiveWith": [], + "name": "groups", + "required": true, + "valueType": "array", + "values": undefined, + }, { "description": "If provided, applies the action to a specific attribute instead of the default one.", "label": "Apply on", "mutuallyExclusiveWith": [], "name": "apply_on", + "required": false, "valueType": "string", "values": [ { @@ -686,6 +1872,7 @@ describe("parseOptionsFromSchema", () => { "label": "Scope", "mutuallyExclusiveWith": [], "name": "scope", + "required": false, "valueType": "string", "values": [ { @@ -703,6 +1890,7 @@ describe("parseOptionsFromSchema", () => { "label": "Group", "mutuallyExclusiveWith": [], "name": "group", + "required": false, "valueType": "string", "values": undefined, }, @@ -777,6 +1965,7 @@ describe("useAvailableOptions", () => { ], "current": [], "disabled": [], + "required": [], } `) }) @@ -834,6 +2023,7 @@ describe("useAvailableOptions", () => { "name": "limit", }, ], + "required": [], } `) }) @@ -860,6 +2050,7 @@ describe("useAvailableOptions", () => { const item = { type: "percentage" as const, selector: "order", + groups: [], value: 0.1, limit: { value: 1, @@ -890,6 +2081,7 @@ describe("useAvailableOptions", () => { "name": "bundle", }, ], + "required": [], } `) }) @@ -931,6 +2123,7 @@ describe("useAvailableOptions", () => { "round", ], "disabled": [], + "required": [], } `) }) @@ -951,6 +2144,7 @@ describe("useAvailableOptions", () => { "available": [], "current": [], "disabled": [], + "required": [], } `) }) diff --git a/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.ts b/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.ts index faa8c91f7..fb7fa8cb3 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.ts +++ b/packages/app-elements/src/ui/forms/RuleEngine/optionsConfig.ts @@ -18,6 +18,8 @@ export interface OptionConfig { description?: string /** Predefined values from configuration */ values?: Array<{ label: string; value: string; meta?: Record }> + /** Whether this option is required and must always be present */ + required?: boolean } /** @@ -36,9 +38,26 @@ export interface OptionsConfig { const configuration = { actions: { "order-rules": { + "": { + selector: true, + quantity: true, + identifiers: [ + { label: "SKU", value: "order.line_items.sku.id" }, + { label: "Bundle", value: "order.line_items.bundle.id" }, + { label: "SKU list", value: "order.line_items.sku.sku_lists.id" }, + ], + }, order: { + selector: true, + groups: true, round: true, + quantity: true, discount_mode: true, + identifiers: [ + { label: "SKU", value: "order.line_items.sku.id" }, + { label: "Bundle", value: "order.line_items.bundle.id" }, + { label: "SKU list", value: "order.line_items.sku.sku_lists.id" }, + ], apply_on: [ { label: "Subtotal amount", value: "subtotal_amount_cents" }, { label: "Total amount", value: "total_amount_cents" }, @@ -81,7 +100,10 @@ const configuration = { ], }, "order.line_items": { + selector: true, + groups: true, round: true, + quantity: true, discount_mode: true, apply_on: [ { label: "Unit amount", value: "unit_amount_cents" }, @@ -124,16 +146,42 @@ const configuration = { }, ], }, - "order.line_items.line_item_options": {}, - "order.line_items.sku": {}, - "order.line_items.bundle": {}, - "order.line_items.shipment": {}, - "order.line_items.payment_method": {}, - "order.line_items.adjustment": {}, - "order.line_items.gift_card": {}, + "order.line_items.line_item_options": { + selector: true, + groups: true, + }, + "order.line_items.sku": { + selector: true, + groups: true, + }, + "order.line_items.bundle": { + selector: true, + groups: true, + }, + "order.line_items.shipment": { + selector: true, + groups: true, + }, + "order.line_items.payment_method": { + selector: true, + groups: true, + }, + "order.line_items.adjustment": { + selector: true, + groups: true, + }, + "order.line_items.gift_card": { + selector: true, + groups: true, + }, } as const, "price-rules": { + "": { + selector: true, + }, price: { + selector: true, + groups: true, apply_on: [ { label: "Amount", value: "amount_cents" }, { label: "Compare at amount", value: "compare_at_amount_cents" }, @@ -209,10 +257,11 @@ type OptionValue = | { label: string value: string - meta?: unknown + meta?: Record }[] type OrderApplyTo = + | "" | "order.line_items.adjustment" | "order.line_items.gift_card" | "order.line_items.shipment" @@ -223,18 +272,22 @@ type OrderApplyTo = | "order.line_items" | "order" -type PriceApplyTo = "price" +type PriceApplyTo = "" | "price" /** * Options that we want to manage dynamically */ const MANAGED_ACTION_OPTIONS = [ + "selector", + "groups", "apply_on", "round", "limit", "discount_mode", "aggregation", "bundle", + "quantity", + "identifiers", ] as const const MANAGED_CONDITION_OPTIONS = ["group", "scope", "aggregations"] as const @@ -249,6 +302,8 @@ export const OPTION_LABELS: Record< ManagedActionOption | ManagedConditionOption, string > = { + selector: "Apply to", + groups: "Groups", apply_on: "Apply on", round: "Round", limit: "Limit", @@ -258,6 +313,8 @@ export const OPTION_LABELS: Record< scope: "Scope", aggregations: "Aggregations", group: "Group", + quantity: "Quantity", + identifiers: "Identifiers", } as const /** @@ -320,25 +377,21 @@ function parseActionOptions( > for (const applyTo of applyToKeys) { - const configForApplyTo = - configForSchemaType[applyTo as keyof typeof configForSchemaType] + const configForApplyTo = configForSchemaType[ + applyTo as keyof typeof configForSchemaType + ] as Record if (configForApplyTo == null) continue // Filter and enhance options based on configuration actionsConfig[actionType][applyTo] = baseOptions .filter((option) => { - return ( - configForApplyTo[ - option.name as ManagedActionOption | ManagedConditionOption - ] != null - ) + return configForApplyTo[option.name] != null }) .map((option) => { - const optionValue = - configForApplyTo[ - option.name as ManagedActionOption | ManagedConditionOption - ] + const optionValue = configForApplyTo[option.name] as + | OptionValue + | undefined return { ...option, // Configuration values override schema-derived values @@ -395,6 +448,8 @@ function parseConditionOptions( meta?: Record }>) : option.values, + // Conditions are never required (only actions can be required) + required: false, } }) } catch (error) { @@ -469,7 +524,7 @@ function buildOptions( ): OptionConfig[] { const options: OptionConfig[] = [] - for (const [optionName, { appearsIn }] of optionsMap.entries()) { + for (const [optionName, { appearsIn, requiredIn }] of optionsMap.entries()) { const mutuallyExclusiveWith = findMutuallyExclusiveOptions( optionName, appearsIn, @@ -497,6 +552,7 @@ function buildOptions( optionName as ManagedActionOption | ManagedConditionOption ], mutuallyExclusiveWith, + required: requiredIn.size > 0, ...metadata, }) } @@ -583,6 +639,8 @@ interface OptionAvailability { disabled: OptionConfig[] /** Options that are currently set */ current: string[] + /** Options that are required (must always be present) */ + required: OptionConfig[] } /** @@ -597,6 +655,7 @@ export function useAvailableOptions( available: [], disabled: [], current: [], + required: [], } } @@ -615,13 +674,17 @@ export function useAvailableOptions( : isPresent }) - // Determine available and disabled options + // Separate required options + const requiredOptions: OptionConfig[] = optionsConfig.filter( + (opt) => opt.required === true && !currentOptions.includes(opt.name), + ) + + // Only show non-required, not-set, not-conflicting options as available const available: OptionConfig[] = [] const disabled: OptionConfig[] = [] for (const option of optionsConfig) { - // Skip if already set - if (currentOptions.includes(option.name)) { + if (currentOptions.includes(option.name) || option.required) { continue } @@ -641,5 +704,6 @@ export function useAvailableOptions( available, disabled, current: currentOptions, + required: requiredOptions, } } diff --git a/packages/app-elements/src/ui/forms/RuleEngine/utils.ts b/packages/app-elements/src/ui/forms/RuleEngine/utils.ts index 746aca5de..e5e9de16b 100644 --- a/packages/app-elements/src/ui/forms/RuleEngine/utils.ts +++ b/packages/app-elements/src/ui/forms/RuleEngine/utils.ts @@ -60,7 +60,7 @@ export async function fetchJsonSchema( : void > { if (domain === "localhost") { - domain = "commercelayer.io" + domain = "commercelayer.co" } switch (jsonSchema) { diff --git a/packages/docs/public/mockServiceWorker.js b/packages/docs/public/mockServiceWorker.js index 77b55e17a..cc46714e0 100644 --- a/packages/docs/public/mockServiceWorker.js +++ b/packages/docs/public/mockServiceWorker.js @@ -2,26 +2,26 @@ /* tslint:disable */ /** - * Mock Service Worker (2.1.7). + * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. - * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = "223d191a56023cd36aa88c802961b911" +const PACKAGE_VERSION = "2.12.8" +const INTEGRITY_CHECKSUM = "4db4a41e972cec1b64cc569c66952d82" const IS_MOCKED_RESPONSE = Symbol("isMockedResponse") const activeClientIds = new Set() -self.addEventListener("install", () => { +addEventListener("install", () => { self.skipWaiting() }) -self.addEventListener("activate", (event) => { +addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()) }) -self.addEventListener("message", async (event) => { - const clientId = event.source.id +addEventListener("message", async (event) => { + const clientId = Reflect.get(event.source || {}, "id") if (!clientId || !self.clients) { return @@ -48,7 +48,10 @@ self.addEventListener("message", async (event) => { case "INTEGRITY_CHECK_REQUEST": { sendToClient(client, { type: "INTEGRITY_CHECK_RESPONSE", - payload: INTEGRITY_CHECKSUM, + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, }) break } @@ -58,16 +61,16 @@ self.addEventListener("message", async (event) => { sendToClient(client, { type: "MOCKING_ENABLED", - payload: true, + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, }) break } - case "MOCK_DEACTIVATE": { - activeClientIds.delete(clientId) - break - } - case "CLIENT_CLOSED": { activeClientIds.delete(clientId) @@ -85,72 +88,96 @@ self.addEventListener("message", async (event) => { } }) -self.addEventListener("fetch", (event) => { - const { request } = event +addEventListener("fetch", (event) => { + const requestInterceptedAt = Date.now() - // Bypass navigation requests. - if (request.mode === "navigate") { + if (event.request.mode === "navigate") { return } - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if (request.cache === "only-if-cached" && request.mode !== "same-origin") { + if ( + event.request.cache === "only-if-cached" && + event.request.mode !== "same-origin" + ) { return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). + // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { return } - // Generate unique request ID. const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId)) + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) }) -async function handleRequest(event, requestId) { +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - ;(async () => { - const responseClone = response.clone() - - sendToClient( - client, - { - type: "RESPONSE", - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: "RESPONSE", + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { type: responseClone.type, status: responseClone.status, statusText: responseClone.statusText, - body: responseClone.body, headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, }, }, - [responseClone.body], - ) - })() + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) } return response } -// Resolve the main client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) + if (activeClientIds.has(event.clientId)) { + return client + } + if (client?.frameType === "top-level") { return client } @@ -171,20 +198,39 @@ async function resolveMainClient(event) { }) } -async function getResponse(event, client, requestId) { - const { request } = event - +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = request.clone() + const requestClone = event.request.clone() function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()) + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get("accept") + if (acceptHeader) { + const values = acceptHeader.split(",").map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== "msw/passthrough", + ) - // Remove internal MSW request header so the passthrough request - // complies with any potential CORS preflight checks on the server. - // Some servers forbid unknown request headers. - delete headers["x-msw-intention"] + if (filteredValues.length > 0) { + headers.set("accept", filteredValues.join(", ")) + } else { + headers.delete("accept") + } + } return fetch(requestClone, { headers }) } @@ -202,37 +248,19 @@ async function getResponse(event, client, requestId) { return passthrough() } - // Bypass requests with the explicit bypass header. - // Such requests can be issued by "ctx.fetch()". - const mswIntention = request.headers.get("x-msw-intention") - if (["bypass", "passthrough"].includes(mswIntention)) { - return passthrough() - } - // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer() + const serializedRequest = await serializeRequest(event.request) const clientMessage = await sendToClient( client, { type: "REQUEST", payload: { id: requestId, - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: requestBuffer, - keepalive: request.keepalive, + interceptedAt: requestInterceptedAt, + ...serializedRequest, }, }, - [requestBuffer], + [serializedRequest.body], ) switch (clientMessage.type) { @@ -240,7 +268,7 @@ async function getResponse(event, client, requestId) { return respondWithMock(clientMessage.data) } - case "MOCK_NOT_FOUND": { + case "PASSTHROUGH": { return passthrough() } } @@ -248,6 +276,12 @@ async function getResponse(event, client, requestId) { return passthrough() } +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() @@ -260,14 +294,18 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data) } - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ) + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) }) } -async function respondWithMock(response) { +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { // Setting response status code to 0 is a no-op. // However, when responding with a "Response.error()", the produced Response // instance will have status code set to 0. Since it's not possible to create @@ -285,3 +323,24 @@ async function respondWithMock(response) { return mockedResponse } + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +}