feat(dashboard,types,promotion,medusa): hide fields on promotions depending on templates (#7746)
**what:** - hides different fields depending on the chosen template - remove operator values API - fixes to edit promotion rules - make currency optional for promotion RESOLVES CORE-2297
This commit is contained in:
@@ -2,7 +2,6 @@ import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||
import {
|
||||
AdminPromotionRuleListResponse,
|
||||
AdminRuleAttributeOptionsListResponse,
|
||||
AdminRuleOperatorOptionsListResponse,
|
||||
AdminRuleValueOptionsListResponse,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -49,7 +48,6 @@ export const promotionsQueryKeys = {
|
||||
ruleValue,
|
||||
query,
|
||||
],
|
||||
listRuleOperators: () => [PROMOTIONS_QUERY_KEY],
|
||||
}
|
||||
|
||||
export const usePromotion = (
|
||||
@@ -107,26 +105,6 @@ export const usePromotions = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotionRuleOperators = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
AdminRuleOperatorOptionsListResponse,
|
||||
Error,
|
||||
AdminRuleOperatorOptionsListResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.listRuleOperators(),
|
||||
queryFn: async () => client.promotions.listRuleOperators(),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotionRuleAttributes = (
|
||||
ruleType: string,
|
||||
promotionType?: string,
|
||||
|
||||
@@ -1151,15 +1151,15 @@
|
||||
"description": "The code your customers will enter during checkout."
|
||||
},
|
||||
"value": {
|
||||
"title": "Value"
|
||||
"title": "Promotion Value"
|
||||
},
|
||||
"value_type": {
|
||||
"fixed": {
|
||||
"title": "Fixed amount",
|
||||
"title": "Promotion Value",
|
||||
"description": "eg. 100"
|
||||
},
|
||||
"percentage": {
|
||||
"title": "Percentage",
|
||||
"title": "Promotion Value",
|
||||
"description": "eg. 8%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
PromotionListRes,
|
||||
PromotionRes,
|
||||
PromotionRuleAttributesListRes,
|
||||
PromotionRuleOperatorsListRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
@@ -112,12 +111,6 @@ async function listPromotionRuleValues(
|
||||
)
|
||||
}
|
||||
|
||||
async function listPromotionRuleOperators() {
|
||||
return getRequest<PromotionRuleOperatorsListRes>(
|
||||
`/admin/promotions/rule-operator-options`
|
||||
)
|
||||
}
|
||||
|
||||
export const promotions = {
|
||||
retrieve: retrievePromotion,
|
||||
list: listPromotions,
|
||||
@@ -129,6 +122,5 @@ export const promotions = {
|
||||
updateRules: updatePromotionRules,
|
||||
listRules: listPromotionRules,
|
||||
listRuleAttributes: listPromotionRuleAttributes,
|
||||
listRuleOperators: listPromotionRuleOperators,
|
||||
listRuleValues: listPromotionRuleValues,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
|
||||
import { Button } from "@medusajs/ui"
|
||||
import i18n from "i18next"
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { RouteDrawer } from "../../../../../../components/route-modal"
|
||||
import { RuleTypeValues } from "../../edit-rules"
|
||||
import { RulesFormField } from "../rules-form-field"
|
||||
import { EditRules, EditRulesType } from "./form-schema"
|
||||
|
||||
type EditPromotionFormProps = {
|
||||
promotion: PromotionDTO
|
||||
@@ -18,31 +17,6 @@ type EditPromotionFormProps = {
|
||||
isSubmitting: boolean
|
||||
}
|
||||
|
||||
const EditRules = zod.object({
|
||||
type: zod.string().optional(),
|
||||
rules: zod.array(
|
||||
zod.object({
|
||||
id: zod.string().optional(),
|
||||
attribute: zod
|
||||
.string()
|
||||
.min(1, { message: i18n.t("promotions.form.required") }),
|
||||
operator: zod
|
||||
.string()
|
||||
.min(1, { message: i18n.t("promotions.form.required") }),
|
||||
values: zod.union([
|
||||
zod.number().min(1, { message: i18n.t("promotions.form.required") }),
|
||||
zod.string().min(1, { message: i18n.t("promotions.form.required") }),
|
||||
zod
|
||||
.array(zod.string())
|
||||
.min(1, { message: i18n.t("promotions.form.required") }),
|
||||
]),
|
||||
required: zod.boolean().optional(),
|
||||
disguised: zod.boolean().optional(),
|
||||
field_type: zod.string().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const EditRulesForm = ({
|
||||
promotion,
|
||||
ruleType,
|
||||
@@ -52,7 +26,7 @@ export const EditRulesForm = ({
|
||||
const { t } = useTranslation()
|
||||
const [rulesToRemove, setRulesToRemove] = useState([])
|
||||
|
||||
const form = useForm<zod.infer<typeof EditRules>>({
|
||||
const form = useForm<EditRulesType>({
|
||||
defaultValues: { rules: [], type: promotion.type },
|
||||
resolver: zodResolver(EditRules),
|
||||
})
|
||||
@@ -64,11 +38,11 @@ export const EditRulesForm = ({
|
||||
<form onSubmit={handleFormSubmit} className="flex h-full flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<RulesFormField
|
||||
form={form}
|
||||
form={form as any}
|
||||
ruleType={ruleType}
|
||||
setRulesToRemove={setRulesToRemove}
|
||||
rulesToRemove={rulesToRemove}
|
||||
promotionId={promotion.id}
|
||||
promotion={promotion}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import i18n from "i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
export const EditRules = z.object({
|
||||
type: z.string().optional(),
|
||||
rules: z.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
attribute: z
|
||||
.string()
|
||||
.min(1, { message: i18n.t("promotions.form.required") }),
|
||||
operator: z
|
||||
.string()
|
||||
.min(1, { message: i18n.t("promotions.form.required") }),
|
||||
values: z.union([
|
||||
z.number().min(1, { message: i18n.t("promotions.form.required") }),
|
||||
z.string().min(1, { message: i18n.t("promotions.form.required") }),
|
||||
z
|
||||
.array(z.string())
|
||||
.min(1, { message: i18n.t("promotions.form.required") }),
|
||||
]),
|
||||
required: z.boolean().optional(),
|
||||
disguised: z.boolean().optional(),
|
||||
field_type: z.string().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export type EditRulesType = z.infer<typeof EditRules>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PromotionRuleResponse } from "@medusajs/types"
|
||||
|
||||
export const generateRuleAttributes = (rules?: PromotionRuleResponse[]) =>
|
||||
(rules || []).map((rule) => ({
|
||||
id: rule.id,
|
||||
required: rule.required,
|
||||
field_type: rule.field_type,
|
||||
disguised: rule.disguised,
|
||||
attribute: rule.attribute!,
|
||||
operator: rule.operator!,
|
||||
values:
|
||||
rule.field_type === "number" || rule.operator === "eq"
|
||||
? typeof rule.values === "object"
|
||||
? rule.values[0]?.value
|
||||
: rule.values
|
||||
: rule?.values?.map((v: { value: string }) => v.value!),
|
||||
}))
|
||||
@@ -42,11 +42,15 @@ export const EditRulesWrapper = ({
|
||||
const { mutateAsync: updatePromotionRules, isPending } =
|
||||
usePromotionUpdateRules(promotion.id, ruleType)
|
||||
|
||||
const handleSubmit = (rulesToRemove?: { id: string }[]) => {
|
||||
const handleSubmit = (
|
||||
rulesToRemove?: { id: string; disguised: boolean; attribute: string }[]
|
||||
) => {
|
||||
return async function (data: { rules: PromotionRuleResponse[] }) {
|
||||
const applicationMethodData: Record<any, any> = {}
|
||||
const { rules: allRules = [] } = data
|
||||
const disguisedRules = allRules.filter((rule) => rule.disguised)
|
||||
const disguisedRulesToRemove =
|
||||
rulesToRemove?.filter((r) => r.disguised) || []
|
||||
|
||||
// For all the rules that were disguised, convert them to actual values in the
|
||||
// database, they are currently all under application_method. If more of these are coming
|
||||
@@ -55,6 +59,10 @@ export const EditRulesWrapper = ({
|
||||
applicationMethodData[rule.attribute] = getRuleValue(rule)
|
||||
}
|
||||
|
||||
for (const rule of disguisedRulesToRemove) {
|
||||
applicationMethodData[rule.attribute] = null
|
||||
}
|
||||
|
||||
// This variable will contain the rules that are actual rule objects, without the disguised
|
||||
// objects
|
||||
const rulesData = allRules.filter((rule) => !rule.disguised)
|
||||
@@ -77,14 +85,14 @@ export const EditRulesWrapper = ({
|
||||
return {
|
||||
attribute: rule.attribute,
|
||||
operator: rule.operator,
|
||||
values: rule.operator === "eq" ? rule.values[0] : rule.values,
|
||||
values: rule.values,
|
||||
} as any
|
||||
}),
|
||||
}))
|
||||
|
||||
rulesToRemove?.length &&
|
||||
(await removePromotionRules({
|
||||
rule_ids: rulesToRemove.map((r) => r.id!),
|
||||
rule_ids: rulesToRemove.map((r) => r.id).filter(Boolean),
|
||||
}))
|
||||
|
||||
rulesToUpdate.length &&
|
||||
|
||||
@@ -5,9 +5,5 @@ export const getRuleValue = (rule: PromotionRuleResponse) => {
|
||||
return parseInt(rule.values as unknown as string)
|
||||
}
|
||||
|
||||
if (rule.field_type === "select") {
|
||||
return rule.values[0]
|
||||
}
|
||||
|
||||
return rule.values
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { PromotionRuleResponse } from "@medusajs/types"
|
||||
import { PromotionDTO } from "@medusajs/types"
|
||||
import { Badge, Button, Heading, Select, Text } from "@medusajs/ui"
|
||||
import { Fragment, useEffect } from "react"
|
||||
import { useFieldArray, UseFormReturn, useWatch } from "react-hook-form"
|
||||
@@ -7,15 +7,15 @@ import { useTranslation } from "react-i18next"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import {
|
||||
usePromotionRuleAttributes,
|
||||
usePromotionRuleOperators,
|
||||
usePromotionRules,
|
||||
} from "../../../../../../hooks/api/promotions"
|
||||
import { CreatePromotionSchemaType } from "../../../../promotion-create/components/create-promotion-form/form-schema"
|
||||
import { generateRuleAttributes } from "../edit-rules-form/utils"
|
||||
import { RuleValueFormField } from "../rule-value-form-field"
|
||||
import { requiredProductRule } from "./constants"
|
||||
|
||||
type RulesFormFieldType = {
|
||||
promotionId?: string
|
||||
promotion?: PromotionDTO
|
||||
form: UseFormReturn<CreatePromotionSchemaType>
|
||||
ruleType: "rules" | "target-rules" | "buy-rules"
|
||||
setRulesToRemove?: any
|
||||
@@ -26,32 +26,17 @@ type RulesFormFieldType = {
|
||||
| "application_method.target_rules"
|
||||
}
|
||||
|
||||
const generateRuleAttributes = (rules?: PromotionRuleResponse[]) =>
|
||||
(rules || []).map((rule) => ({
|
||||
id: rule.id,
|
||||
required: rule.required,
|
||||
field_type: rule.field_type,
|
||||
disguised: rule.disguised,
|
||||
attribute: rule.attribute!,
|
||||
operator: rule.operator!,
|
||||
values:
|
||||
rule.field_type === "number"
|
||||
? rule.values
|
||||
: rule?.values?.map((v: { value: string }) => v.value!),
|
||||
}))
|
||||
|
||||
export const RulesFormField = ({
|
||||
form,
|
||||
ruleType,
|
||||
setRulesToRemove,
|
||||
rulesToRemove,
|
||||
scope = "rules",
|
||||
promotionId,
|
||||
promotion,
|
||||
}: RulesFormFieldType) => {
|
||||
const { t } = useTranslation()
|
||||
const formData = form.getValues()
|
||||
const { attributes } = usePromotionRuleAttributes(ruleType, formData.type)
|
||||
const { operators } = usePromotionRuleOperators()
|
||||
|
||||
const { fields, append, remove, update, replace } = useFieldArray({
|
||||
control: form.control,
|
||||
@@ -59,21 +44,31 @@ export const RulesFormField = ({
|
||||
keyName: scope,
|
||||
})
|
||||
|
||||
const promotionType: string = useWatch({
|
||||
const promotionType = useWatch({
|
||||
control: form.control,
|
||||
name: "type",
|
||||
defaultValue: promotion?.type,
|
||||
})
|
||||
|
||||
const applicationMethodType = useWatch({
|
||||
control: form.control,
|
||||
name: "application_method.type",
|
||||
defaultValue: promotion?.application_method?.type,
|
||||
})
|
||||
|
||||
const query: Record<string, string> = promotionType
|
||||
? { promotion_type: promotionType }
|
||||
? {
|
||||
promotion_type: promotionType,
|
||||
application_method_type: applicationMethodType,
|
||||
}
|
||||
: {}
|
||||
|
||||
const { rules, isLoading } = usePromotionRules(
|
||||
promotionId || null,
|
||||
promotion?.id || null,
|
||||
ruleType,
|
||||
query,
|
||||
{
|
||||
enabled: !!promotionType,
|
||||
enabled: !!promotion?.id || (!!promotionType && !!applicationMethodType),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -104,12 +99,18 @@ export const RulesFormField = ({
|
||||
promotionType === "buyget"
|
||||
) {
|
||||
form.resetField(scope)
|
||||
const rulesToAppend = promotionId
|
||||
const rulesToAppend = promotion?.id
|
||||
? rules
|
||||
: [...rules, requiredProductRule]
|
||||
|
||||
replace(generateRuleAttributes(rulesToAppend) as any)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
form.resetField(scope)
|
||||
|
||||
replace(generateRuleAttributes(rules) as any)
|
||||
}, [promotionType, isLoading])
|
||||
|
||||
return (
|
||||
@@ -165,10 +166,18 @@ export const RulesFormField = ({
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
update(index, { ...fieldRule, values: [] })
|
||||
const currentAttributeOption =
|
||||
attributeOptions.find((ao) => ao.id === e)
|
||||
|
||||
update(index, {
|
||||
...fieldRule,
|
||||
values: [],
|
||||
disguised:
|
||||
currentAttributeOption?.disguised || false,
|
||||
})
|
||||
onChange(e)
|
||||
}}
|
||||
disabled={fieldRule.disguised}
|
||||
disabled={fieldRule.required}
|
||||
>
|
||||
<Select.Trigger
|
||||
ref={attributeRef}
|
||||
@@ -206,14 +215,14 @@ export const RulesFormField = ({
|
||||
key={`${identifier}.${scope}.${operatorsField.name}`}
|
||||
{...operatorsField}
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
const currentAttributeOption = attributes.find(
|
||||
(attr) => attr.value === fieldRule.attribute
|
||||
)
|
||||
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Control>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
disabled={fieldRule.disguised}
|
||||
>
|
||||
<Select {...field} onValueChange={onChange}>
|
||||
<Select.Trigger
|
||||
ref={operatorRef}
|
||||
className="bg-ui-bg-base"
|
||||
@@ -222,16 +231,18 @@ export const RulesFormField = ({
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content>
|
||||
{operators?.map((c, i) => (
|
||||
<Select.Item
|
||||
key={`${identifier}-operator-option-${i}`}
|
||||
value={c.value}
|
||||
>
|
||||
<span className="text-ui-fg-subtle">
|
||||
{c.label}
|
||||
</span>
|
||||
</Select.Item>
|
||||
))}
|
||||
{currentAttributeOption?.operators?.map(
|
||||
(c, i) => (
|
||||
<Select.Item
|
||||
key={`${identifier}-operator-option-${i}`}
|
||||
value={c.value}
|
||||
>
|
||||
<span className="text-ui-fg-subtle">
|
||||
{c.label}
|
||||
</span>
|
||||
</Select.Item>
|
||||
)
|
||||
)}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
@@ -262,8 +273,7 @@ export const RulesFormField = ({
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!fieldRule.required) {
|
||||
fieldRule.id &&
|
||||
setRulesToRemove &&
|
||||
setRulesToRemove &&
|
||||
setRulesToRemove([...rulesToRemove, fieldRule])
|
||||
|
||||
remove(index)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
clx,
|
||||
CurrencyInput,
|
||||
@@ -15,7 +16,13 @@ import { useForm, useWatch } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import { PromotionRuleOperatorValues } from "@medusajs/types"
|
||||
import {
|
||||
ApplicationMethodAllocationValues,
|
||||
ApplicationMethodTargetTypeValues,
|
||||
ApplicationMethodTypeValues,
|
||||
PromotionRuleOperatorValues,
|
||||
PromotionTypeValues,
|
||||
} from "@medusajs/types"
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
|
||||
@@ -33,6 +40,25 @@ import { Tab } from "./constants"
|
||||
import { CreatePromotionSchema } from "./form-schema"
|
||||
import { templates } from "./templates"
|
||||
|
||||
const defaultValues = {
|
||||
campaign_id: undefined,
|
||||
template_id: templates[0].id!,
|
||||
campaign_choice: "none" as "none",
|
||||
is_automatic: "false",
|
||||
code: "",
|
||||
type: "standard" as PromotionTypeValues,
|
||||
rules: [],
|
||||
application_method: {
|
||||
allocation: "each" as ApplicationMethodAllocationValues,
|
||||
type: "fixed" as ApplicationMethodTypeValues,
|
||||
target_type: "items" as ApplicationMethodTargetTypeValues,
|
||||
max_quantity: 1,
|
||||
target_rules: [],
|
||||
buy_rules: [],
|
||||
},
|
||||
campaign: undefined,
|
||||
}
|
||||
|
||||
export const CreatePromotionForm = () => {
|
||||
const [tab, setTab] = useState<Tab>(Tab.TYPE)
|
||||
const [detailsValidated, setDetailsValidated] = useState(false)
|
||||
@@ -41,24 +67,7 @@ export const CreatePromotionForm = () => {
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof CreatePromotionSchema>>({
|
||||
defaultValues: {
|
||||
campaign_id: undefined,
|
||||
template_id: templates[0].id!,
|
||||
campaign_choice: "none",
|
||||
is_automatic: "false",
|
||||
code: "",
|
||||
type: "standard",
|
||||
rules: [],
|
||||
application_method: {
|
||||
allocation: "each",
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
max_quantity: 1,
|
||||
target_rules: [],
|
||||
buy_rules: [],
|
||||
},
|
||||
campaign: undefined,
|
||||
},
|
||||
defaultValues,
|
||||
resolver: zodResolver(CreatePromotionSchema),
|
||||
})
|
||||
|
||||
@@ -172,7 +181,7 @@ export const CreatePromotionForm = () => {
|
||||
name: "template_id",
|
||||
})
|
||||
|
||||
useMemo(() => {
|
||||
const currentTemplate = useMemo(() => {
|
||||
const currentTemplate = templates.find(
|
||||
(template) => template.id === watchTemplateId
|
||||
)
|
||||
@@ -181,6 +190,8 @@ export const CreatePromotionForm = () => {
|
||||
return
|
||||
}
|
||||
|
||||
form.reset({ ...defaultValues, template_id: watchTemplateId })
|
||||
|
||||
for (const [key, value] of Object.entries(currentTemplate.defaults)) {
|
||||
if (typeof value === "object") {
|
||||
for (const [subKey, subValue] of Object.entries(value)) {
|
||||
@@ -190,6 +201,8 @@ export const CreatePromotionForm = () => {
|
||||
form.setValue(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return currentTemplate
|
||||
}, [watchTemplateId])
|
||||
|
||||
const watchValueType = useWatch({
|
||||
@@ -215,6 +228,14 @@ export const CreatePromotionForm = () => {
|
||||
})
|
||||
|
||||
const isTypeStandard = watchType === "standard"
|
||||
|
||||
const targetType = useWatch({
|
||||
control: form.control,
|
||||
name: "application_method.target_type",
|
||||
})
|
||||
|
||||
const isTargetTypeOrder = targetType === "order"
|
||||
|
||||
const formData = form.getValues()
|
||||
let campaignQuery: object = {}
|
||||
|
||||
@@ -385,9 +406,22 @@ export const CreatePromotionForm = () => {
|
||||
|
||||
<ProgressTabs.Content
|
||||
value={Tab.PROMOTION}
|
||||
className="flex flex-1 flex-col gap-10"
|
||||
className="flex flex-1 flex-col gap-8"
|
||||
>
|
||||
<Heading level="h2">{t(`promotions.sections.details`)}</Heading>
|
||||
<Heading level="h1" className="text-fg-base">
|
||||
{t(`promotions.sections.details`)}
|
||||
|
||||
{currentTemplate?.title && (
|
||||
<Badge
|
||||
className="ml-2 align-middle"
|
||||
color="grey"
|
||||
size="2xsmall"
|
||||
rounded="full"
|
||||
>
|
||||
{currentTemplate?.title}
|
||||
</Badge>
|
||||
)}
|
||||
</Heading>
|
||||
|
||||
{form.formState.errors.root && (
|
||||
<Alert
|
||||
@@ -471,167 +505,14 @@ export const CreatePromotionForm = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.fields.type")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex gap-y-3"
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"standard"}
|
||||
label={t("promotions.form.type.standard.title")}
|
||||
description={t(
|
||||
"promotions.form.type.standard.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"buyget"}
|
||||
label={t("promotions.form.type.buyget.title")}
|
||||
description={t(
|
||||
"promotions.form.type.buyget.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<RulesFormField form={form} ruleType={"rules"} />
|
||||
|
||||
<Divider />
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.fields.value_type")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex gap-y-3"
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"fixed"}
|
||||
label={t("promotions.form.value_type.fixed.title")}
|
||||
description={t(
|
||||
"promotions.form.value_type.fixed.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"percentage"}
|
||||
label={t(
|
||||
"promotions.form.value_type.percentage.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.value_type.percentage.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex gap-y-4">
|
||||
{!currentTemplate?.hiddenFields?.includes("type") && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.value"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
const currencyCode =
|
||||
form.getValues().application_method.currency_code
|
||||
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Label
|
||||
tooltip={
|
||||
currencyCode || !isFixedValueType
|
||||
? undefined
|
||||
: t("promotions.fields.amount.tooltip")
|
||||
}
|
||||
>
|
||||
{isFixedValueType
|
||||
? t("fields.amount")
|
||||
: t("fields.percentage")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
{isFixedValueType ? (
|
||||
<CurrencyInput
|
||||
{...field}
|
||||
min={0}
|
||||
onValueChange={(value) => {
|
||||
onChange(value ? parseInt(value) : "")
|
||||
}}
|
||||
code={currencyCode}
|
||||
symbol={
|
||||
currencyCode
|
||||
? getCurrencySymbol(currencyCode)
|
||||
: ""
|
||||
}
|
||||
value={value}
|
||||
disabled={!currencyCode}
|
||||
/>
|
||||
) : (
|
||||
<PercentageInput
|
||||
key="amount"
|
||||
className="text-right"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e.target.value === ""
|
||||
? null
|
||||
: parseInt(e.target.value)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isTypeStandard && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.allocation"
|
||||
name="type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.fields.allocation")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Label>{t("promotions.fields.type")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex gap-y-3"
|
||||
@@ -639,21 +520,19 @@ export const CreatePromotionForm = () => {
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"each"}
|
||||
label={t("promotions.form.allocation.each.title")}
|
||||
value={"standard"}
|
||||
label={t("promotions.form.type.standard.title")}
|
||||
description={t(
|
||||
"promotions.form.allocation.each.description"
|
||||
"promotions.form.type.standard.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"across"}
|
||||
label={t(
|
||||
"promotions.form.allocation.across.title"
|
||||
)}
|
||||
value={"buyget"}
|
||||
label={t("promotions.form.type.buyget.title")}
|
||||
description={t(
|
||||
"promotions.form.allocation.across.description"
|
||||
"promotions.form.type.buyget.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
@@ -666,8 +545,141 @@ export const CreatePromotionForm = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTypeStandard && watchAllocation === "each" && (
|
||||
<div className="flex gap-y-4">
|
||||
<Divider />
|
||||
|
||||
<RulesFormField form={form} ruleType={"rules"} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{!currentTemplate?.hiddenFields?.includes(
|
||||
"application_method.type"
|
||||
) && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.fields.value_type")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex gap-y-3"
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"fixed"}
|
||||
label={t(
|
||||
"promotions.form.value_type.fixed.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.value_type.fixed.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"percentage"}
|
||||
label={t(
|
||||
"promotions.form.value_type.percentage.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.value_type.percentage.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-y-4 gap-x-2">
|
||||
{!currentTemplate?.hiddenFields?.includes(
|
||||
"application_method.value"
|
||||
) && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.value"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
const currencyCode =
|
||||
form.getValues().application_method.currency_code
|
||||
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Label
|
||||
tooltip={
|
||||
currencyCode || !isFixedValueType
|
||||
? undefined
|
||||
: t("promotions.fields.amount.tooltip")
|
||||
}
|
||||
>
|
||||
{t("promotions.form.value.title")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
{isFixedValueType ? (
|
||||
<CurrencyInput
|
||||
{...field}
|
||||
min={0}
|
||||
onValueChange={(value) => {
|
||||
onChange(value ? parseInt(value) : "")
|
||||
}}
|
||||
code={currencyCode}
|
||||
symbol={
|
||||
currencyCode
|
||||
? getCurrencySymbol(currencyCode)
|
||||
: ""
|
||||
}
|
||||
value={value}
|
||||
disabled={!currencyCode}
|
||||
/>
|
||||
) : (
|
||||
<PercentageInput
|
||||
key="amount"
|
||||
className="text-right"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e.target.value === ""
|
||||
? null
|
||||
: parseInt(e.target.value)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey={
|
||||
isFixedValueType
|
||||
? "promotions.form.value_type.fixed.description"
|
||||
: "promotions.form.value_type.percentage.description"
|
||||
}
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTypeStandard && watchAllocation === "each" && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.max_quantity"
|
||||
@@ -705,10 +717,58 @@ export const CreatePromotionForm = () => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
{isTypeStandard &&
|
||||
!currentTemplate?.hiddenFields?.includes(
|
||||
"application_method.allocation"
|
||||
) && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.allocation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.fields.allocation")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex gap-y-3"
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"each"}
|
||||
label={t(
|
||||
"promotions.form.allocation.each.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.allocation.each.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"across"}
|
||||
label={t(
|
||||
"promotions.form.allocation.across.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.allocation.across.description"
|
||||
)}
|
||||
className={clx("basis-1/2")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isTypeStandard && (
|
||||
<>
|
||||
@@ -717,16 +777,19 @@ export const CreatePromotionForm = () => {
|
||||
ruleType={"buy-rules"}
|
||||
scope="application_method.buy_rules"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<RulesFormField
|
||||
form={form}
|
||||
ruleType={"target-rules"}
|
||||
scope="application_method.target_rules"
|
||||
/>
|
||||
{!isTargetTypeOrder && (
|
||||
<>
|
||||
<Divider />
|
||||
<RulesFormField
|
||||
form={form}
|
||||
ruleType={"target-rules"}
|
||||
scope="application_method.target_rules"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ProgressTabs.Content>
|
||||
|
||||
<ProgressTabs.Content
|
||||
|
||||
@@ -29,7 +29,7 @@ export const CreatePromotionSchema = z
|
||||
application_method: z.object({
|
||||
allocation: z.enum(["each", "across"]),
|
||||
value: z.number().min(0),
|
||||
currency_code: z.string(),
|
||||
currency_code: z.string().optional(),
|
||||
max_quantity: z.number().optional().nullable(),
|
||||
target_rules: RuleSchema,
|
||||
buy_rules: RuleSchema,
|
||||
@@ -54,3 +54,5 @@ export const CreatePromotionSchema = z
|
||||
message: `required field`,
|
||||
}
|
||||
)
|
||||
|
||||
export type CreatePromotionSchemaType = z.infer<typeof CreatePromotionSchema>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
const commonHiddenFields = [
|
||||
"type",
|
||||
"application_method.type",
|
||||
"application_method.allocation",
|
||||
]
|
||||
|
||||
export const templates = [
|
||||
{
|
||||
id: "amount_off_products",
|
||||
type: "standard",
|
||||
title: "Amount off products",
|
||||
description: "Discount specific products or collection of products",
|
||||
hiddenFields: [...commonHiddenFields],
|
||||
defaults: {
|
||||
is_automatic: "false",
|
||||
type: "standard",
|
||||
@@ -19,6 +26,7 @@ export const templates = [
|
||||
type: "standard",
|
||||
title: "Amount off order",
|
||||
description: "Discounts the total order amount",
|
||||
hiddenFields: [...commonHiddenFields],
|
||||
defaults: {
|
||||
is_automatic: "false",
|
||||
type: "standard",
|
||||
@@ -34,6 +42,7 @@ export const templates = [
|
||||
type: "standard",
|
||||
title: "Percentage off product",
|
||||
description: "Discounts a percentage off selected products",
|
||||
hiddenFields: [...commonHiddenFields],
|
||||
defaults: {
|
||||
is_automatic: "false",
|
||||
type: "standard",
|
||||
@@ -49,12 +58,13 @@ export const templates = [
|
||||
type: "standard",
|
||||
title: "Percentage off order",
|
||||
description: "Discounts a percentage of the total order amount",
|
||||
hiddenFields: [...commonHiddenFields],
|
||||
defaults: {
|
||||
is_automatic: "false",
|
||||
type: "standard",
|
||||
application_method: {
|
||||
allocation: "across",
|
||||
target_type: "items",
|
||||
target_type: "order",
|
||||
type: "percentage",
|
||||
},
|
||||
},
|
||||
@@ -64,6 +74,7 @@ export const templates = [
|
||||
type: "buy_get",
|
||||
title: "Buy X Get Y",
|
||||
description: "Buy X product(s), get Y product(s)",
|
||||
hiddenFields: [...commonHiddenFields, "application_method.value"],
|
||||
defaults: {
|
||||
is_automatic: "false",
|
||||
type: "buyget",
|
||||
|
||||
@@ -82,7 +82,7 @@ export const PromotionConditionsSection = ({
|
||||
className="h-[180px]"
|
||||
title="No records yet."
|
||||
message="Please check back later or add a target condition today"
|
||||
action={{ to: "/promotions", label: "Add condition" }}
|
||||
action={{ to: `${ruleType}/edit`, label: "Add condition" }}
|
||||
buttonVariant="transparentIconLeft"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,11 @@ export interface RuleAttributeOptionsResponse {
|
||||
field_type: string
|
||||
required: boolean
|
||||
disguised: boolean
|
||||
operators: {
|
||||
id: string
|
||||
value: string
|
||||
label: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface AdminRuleAttributeOptionsListResponse {
|
||||
|
||||
@@ -124,7 +124,7 @@ export interface CreateApplicationMethodDTO {
|
||||
/**
|
||||
* Currency of the value to apply.
|
||||
*/
|
||||
currency_code: string
|
||||
currency_code?: string
|
||||
|
||||
/**
|
||||
* The max quantity allowed in the cart for the associated promotion to be applied.
|
||||
|
||||
@@ -33,9 +33,11 @@ export const GET = async (
|
||||
})
|
||||
|
||||
const [promotion] = await remoteQuery(queryObject)
|
||||
const ruleAttributes = getRuleAttributesMap(
|
||||
promotion?.type || req.query.promotion_type
|
||||
)[ruleType]
|
||||
const ruleAttributes = getRuleAttributesMap({
|
||||
promotionType: promotion?.type || req.query.promotion_type,
|
||||
applicationMethodType:
|
||||
promotion?.application_method?.type || req.query.application_method_type,
|
||||
})[ruleType]
|
||||
const promotionRules: any[] = []
|
||||
|
||||
if (dasherizedRuleType === RuleType.RULES) {
|
||||
@@ -48,7 +50,6 @@ export const GET = async (
|
||||
|
||||
const transformedRules: AdminGetPromotionRulesRes = []
|
||||
const disguisedRules = ruleAttributes.filter((attr) => !!attr.disguised)
|
||||
const requiredRules = ruleAttributes.filter((attr) => !!attr.required)
|
||||
|
||||
for (const disguisedRule of disguisedRules) {
|
||||
const getValues = () => {
|
||||
@@ -65,18 +66,22 @@ export const GET = async (
|
||||
return []
|
||||
}
|
||||
|
||||
transformedRules.push({
|
||||
id: undefined,
|
||||
attribute: disguisedRule.id,
|
||||
attribute_label: disguisedRule.label,
|
||||
field_type: disguisedRule.field_type,
|
||||
hydrate: disguisedRule.hydrate || false,
|
||||
operator: RuleOperator.EQ,
|
||||
operator_label: operatorsMap[RuleOperator.EQ].label,
|
||||
values: getValues(),
|
||||
disguised: true,
|
||||
required: true,
|
||||
})
|
||||
const required = disguisedRule.required ?? true
|
||||
const applicationMethod = promotion?.application_method
|
||||
const recordValue = applicationMethod?.[disguisedRule.id]
|
||||
|
||||
if (required || recordValue) {
|
||||
transformedRules.push({
|
||||
...disguisedRule,
|
||||
id: undefined,
|
||||
attribute: disguisedRule.id,
|
||||
attribute_label: disguisedRule.label,
|
||||
operator: RuleOperator.EQ,
|
||||
operator_label: operatorsMap[RuleOperator.EQ].label,
|
||||
value: undefined,
|
||||
values: getValues(),
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -125,36 +130,15 @@ export const GET = async (
|
||||
|
||||
if (!currentRuleAttribute.hydrate) {
|
||||
transformedRules.push({
|
||||
...currentRuleAttribute,
|
||||
...promotionRule,
|
||||
attribute_label: currentRuleAttribute.label,
|
||||
field_type: currentRuleAttribute.field_type,
|
||||
operator_label:
|
||||
operatorsMap[promotionRule.operator]?.label || promotionRule.operator,
|
||||
disguised: false,
|
||||
required: currentRuleAttribute.required || false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (requiredRules.length && !transformedRules.length) {
|
||||
for (const requiredRule of requiredRules) {
|
||||
transformedRules.push({
|
||||
id: undefined,
|
||||
attribute: requiredRule.value,
|
||||
attribute_label: requiredRule.label,
|
||||
operator: RuleOperator.EQ,
|
||||
field_type: requiredRule.field_type,
|
||||
operator_label: operatorsMap[RuleOperator.EQ].label,
|
||||
values: [],
|
||||
disguised: true,
|
||||
required: true,
|
||||
hydrate: false,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
rules: transformedRules,
|
||||
})
|
||||
|
||||
@@ -14,7 +14,10 @@ export const GET = async (
|
||||
validateRuleType(ruleType)
|
||||
|
||||
const attributes =
|
||||
getRuleAttributesMap(req.query.promotion_type as string)[ruleType] || []
|
||||
getRuleAttributesMap({
|
||||
promotionType: req.query.promotion_type as string,
|
||||
applicationMethodType: req.query.application_method_type as string,
|
||||
})[ruleType] || []
|
||||
|
||||
res.json({
|
||||
attributes,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
import { operatorsMap } from "../utils"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
operators: Object.values(operatorsMap),
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
} from "../../../utils"
|
||||
import { AdminGetPromotionRuleParamsType } from "../../../validators"
|
||||
|
||||
/*
|
||||
This endpoint returns all the potential values for rules (promotion rules, target rules and buy rules)
|
||||
given an attribute of a rule. The response for different rule_attributes are returned uniformly
|
||||
as an array of labels and values.
|
||||
Eg. If the rule_attribute requested is "currency_code" for "rules" rule type, we return currencies
|
||||
from the currency module.
|
||||
*/
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest<AdminGetPromotionRuleParamsType>,
|
||||
res: MedusaResponse
|
||||
@@ -21,6 +28,7 @@ export const GET = async (
|
||||
rule_type: ruleType,
|
||||
rule_attribute_id: ruleAttributeId,
|
||||
promotion_type: promotionType,
|
||||
application_method_type: applicationMethodType,
|
||||
} = req.params
|
||||
const queryConfig = ruleQueryConfigurations[ruleAttributeId]
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
@@ -33,7 +41,12 @@ export const GET = async (
|
||||
}
|
||||
|
||||
validateRuleType(ruleType)
|
||||
validateRuleAttribute(promotionType, ruleType, ruleAttributeId)
|
||||
validateRuleAttribute({
|
||||
promotionType,
|
||||
ruleType,
|
||||
ruleAttributeId,
|
||||
applicationMethodType,
|
||||
})
|
||||
|
||||
const { rows } = await remoteQuery(
|
||||
remoteQueryObjectFromString({
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { PromotionType } from "@medusajs/utils"
|
||||
import {
|
||||
ApplicationMethodType,
|
||||
PromotionType,
|
||||
RuleOperator,
|
||||
} from "@medusajs/utils"
|
||||
import { operatorsMap } from "./operators-map"
|
||||
|
||||
export enum DisguisedRule {
|
||||
APPLY_TO_QUANTITY = "apply_to_quantity",
|
||||
@@ -7,21 +12,13 @@ export enum DisguisedRule {
|
||||
}
|
||||
|
||||
const ruleAttributes = [
|
||||
{
|
||||
id: DisguisedRule.CURRENCY_CODE,
|
||||
value: DisguisedRule.CURRENCY_CODE,
|
||||
label: "Currency Code",
|
||||
field_type: "select",
|
||||
required: true,
|
||||
disguised: true,
|
||||
hydrate: true,
|
||||
},
|
||||
{
|
||||
id: "customer_group",
|
||||
value: "customer.groups.id",
|
||||
label: "Customer Group",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "region",
|
||||
@@ -29,6 +26,7 @@ const ruleAttributes = [
|
||||
label: "Region",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
@@ -36,6 +34,7 @@ const ruleAttributes = [
|
||||
label: "Country",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "sales_channel",
|
||||
@@ -43,6 +42,7 @@ const ruleAttributes = [
|
||||
label: "Sales Channel",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -53,6 +53,7 @@ const commonAttributes = [
|
||||
label: "Product",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "product_category",
|
||||
@@ -60,6 +61,7 @@ const commonAttributes = [
|
||||
label: "Product Category",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "product_collection",
|
||||
@@ -67,6 +69,7 @@ const commonAttributes = [
|
||||
label: "Product Collection",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "product_type",
|
||||
@@ -74,6 +77,7 @@ const commonAttributes = [
|
||||
label: "Product Type",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
{
|
||||
id: "product_tag",
|
||||
@@ -81,9 +85,21 @@ const commonAttributes = [
|
||||
label: "Product Tag",
|
||||
required: false,
|
||||
field_type: "multiselect",
|
||||
operators: Object.values(operatorsMap),
|
||||
},
|
||||
]
|
||||
|
||||
const currencyRule = {
|
||||
id: DisguisedRule.CURRENCY_CODE,
|
||||
value: DisguisedRule.CURRENCY_CODE,
|
||||
label: "Currency Code",
|
||||
field_type: "select",
|
||||
required: true,
|
||||
disguised: true,
|
||||
hydrate: true,
|
||||
operators: [operatorsMap[RuleOperator.EQ]],
|
||||
}
|
||||
|
||||
const buyGetBuyRules = [
|
||||
{
|
||||
id: DisguisedRule.BUY_RULES_MIN_QUANTITY,
|
||||
@@ -92,6 +108,7 @@ const buyGetBuyRules = [
|
||||
field_type: "number",
|
||||
required: true,
|
||||
disguised: true,
|
||||
operators: [operatorsMap[RuleOperator.EQ]],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -103,16 +120,29 @@ const buyGetTargetRules = [
|
||||
field_type: "number",
|
||||
required: true,
|
||||
disguised: true,
|
||||
operators: [operatorsMap[RuleOperator.EQ]],
|
||||
},
|
||||
]
|
||||
|
||||
export const getRuleAttributesMap = (promotionType?: string) => {
|
||||
export const getRuleAttributesMap = ({
|
||||
promotionType,
|
||||
applicationMethodType,
|
||||
}: {
|
||||
promotionType?: string
|
||||
applicationMethodType?: string
|
||||
}) => {
|
||||
const map = {
|
||||
rules: [...ruleAttributes],
|
||||
"target-rules": [...commonAttributes],
|
||||
"buy-rules": [...commonAttributes],
|
||||
}
|
||||
|
||||
if (applicationMethodType === ApplicationMethodType.FIXED) {
|
||||
map["rules"].push({ ...currencyRule })
|
||||
} else {
|
||||
map["rules"].push({ ...currencyRule, required: false })
|
||||
}
|
||||
|
||||
if (promotionType === PromotionType.BUYGET) {
|
||||
map["buy-rules"].push(...buyGetBuyRules)
|
||||
map["target-rules"].push(...buyGetTargetRules)
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { getRuleAttributesMap } from "./rule-attributes-map"
|
||||
|
||||
export function validateRuleAttribute(
|
||||
promotionType: string | undefined,
|
||||
ruleType: string,
|
||||
export function validateRuleAttribute(attributes: {
|
||||
promotionType: string | undefined
|
||||
ruleType: string
|
||||
ruleAttributeId: string
|
||||
) {
|
||||
const ruleAttributes = getRuleAttributesMap(promotionType)[ruleType] || []
|
||||
applicationMethodType?: string
|
||||
}) {
|
||||
const { promotionType, ruleType, ruleAttributeId, applicationMethodType } =
|
||||
attributes
|
||||
|
||||
const ruleAttributes =
|
||||
getRuleAttributesMap({
|
||||
promotionType,
|
||||
applicationMethodType,
|
||||
})[ruleType] || []
|
||||
|
||||
const ruleAttribute = ruleAttributes.find((obj) => obj.id === ruleAttributeId)
|
||||
|
||||
if (!ruleAttribute) {
|
||||
|
||||
@@ -49,6 +49,7 @@ export type AdminGetPromotionRuleParamsType = z.infer<
|
||||
>
|
||||
export const AdminGetPromotionRuleParams = z.object({
|
||||
promotion_type: z.string().optional(),
|
||||
application_method_type: z.string().optional(),
|
||||
})
|
||||
|
||||
export type AdminGetPromotionRuleTypeParamsType = z.infer<
|
||||
@@ -57,6 +58,7 @@ export type AdminGetPromotionRuleTypeParamsType = z.infer<
|
||||
export const AdminGetPromotionRuleTypeParams = createSelectParams().merge(
|
||||
z.object({
|
||||
promotion_type: z.string().optional(),
|
||||
application_method_type: z.string().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -105,7 +107,7 @@ export const AdminCreateApplicationMethod = z
|
||||
.object({
|
||||
description: z.string().optional(),
|
||||
value: z.number(),
|
||||
currency_code: z.string(),
|
||||
currency_code: z.string().optional().nullable(),
|
||||
max_quantity: z.number().optional().nullable(),
|
||||
type: z.nativeEnum(ApplicationMethodType),
|
||||
target_type: z.nativeEnum(ApplicationMethodTargetType),
|
||||
@@ -125,7 +127,7 @@ export const AdminUpdateApplicationMethod = z
|
||||
description: z.string().optional(),
|
||||
value: z.number().optional(),
|
||||
max_quantity: z.number().optional().nullable(),
|
||||
currency_code: z.string().optional(),
|
||||
currency_code: z.string().optional().nullable(),
|
||||
type: z.nativeEnum(ApplicationMethodType).optional(),
|
||||
target_type: z.nativeEnum(ApplicationMethodTargetType).optional(),
|
||||
allocation: z.nativeEnum(ApplicationMethodAllocation).optional(),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240617102917 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "currency_code" type text using ("currency_code"::text);'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "currency_code" drop not null;'
|
||||
)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "currency_code" type text using ("currency_code"::text);'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "promotion_application_method" alter column "currency_code" set not null;'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export interface CreateApplicationMethodDTO {
|
||||
target_type: ApplicationMethodTargetTypeValues
|
||||
allocation?: ApplicationMethodAllocationValues
|
||||
value?: BigNumberInput
|
||||
currency_code: string
|
||||
currency_code?: string | null
|
||||
promotion: Promotion | string | PromotionDTO
|
||||
max_quantity?: BigNumberInput | null
|
||||
buy_rules_min_quantity?: BigNumberInput | null
|
||||
@@ -26,7 +26,7 @@ export interface UpdateApplicationMethodDTO {
|
||||
target_type?: ApplicationMethodTargetTypeValues
|
||||
allocation?: ApplicationMethodAllocationValues
|
||||
value?: BigNumberInput
|
||||
currency_code?: string
|
||||
currency_code?: string | null
|
||||
promotion?: Promotion | string | PromotionDTO
|
||||
max_quantity?: BigNumberInput | null
|
||||
buy_rules_min_quantity?: BigNumberInput | null
|
||||
|
||||
Reference in New Issue
Block a user