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:
Riqwan Thamir authored and GitHub committed 2024-06-18 16:47:42 +00:00
1 parent 1451112f08
commit 0d04c548f5
24 files changed
+523 -389

No files matched your search

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