chore: buyget templates add default target + buy rules (#7500)

* chore: buyget templates add default target + buy rules

* chore: reposition

* chore: address comments

* chore: added fixes

* chore: fix typo

* chore: fix strictness checks
This commit is contained in:
Riqwan Thamir
2024-06-07 09:47:31 +02:00
committed by GitHub
parent bd302e678e
commit 1f1b996f63
23 changed files with 322 additions and 445 deletions

View File

@@ -1,5 +1,10 @@
import { AdminGetPromotionsParams } from "@medusajs/medusa"
import { AdminRuleValueOptionsListResponse } from "@medusajs/types"
import {
AdminPromotionRuleListResponse,
AdminRuleAttributeOptionsListResponse,
AdminRuleOperatorOptionsListResponse,
AdminRuleValueOptionsListResponse,
} from "@medusajs/types"
import {
QueryKey,
useMutation,
@@ -21,21 +26,23 @@ import {
PromotionDeleteRes,
PromotionListRes,
PromotionRes,
PromotionRuleAttributesListRes,
PromotionRuleOperatorsListRes,
PromotionRulesListRes,
} from "../../types/api-responses"
import { campaignsQueryKeys } from "./campaigns"
const PROMOTIONS_QUERY_KEY = "promotions" as const
export const promotionsQueryKeys = {
...queryKeysFactory(PROMOTIONS_QUERY_KEY),
listRules: (id: string | null, ruleType: string) => [
// TODO: handle invalidations properly
listRules: (
id: string | null,
ruleType: string,
query?: Record<string, string>
) => [PROMOTIONS_QUERY_KEY, id, ruleType, query],
listRuleAttributes: (ruleType: string, promotionType?: string) => [
PROMOTIONS_QUERY_KEY,
id,
ruleType,
promotionType,
],
listRuleAttributes: (ruleType: string) => [PROMOTIONS_QUERY_KEY, ruleType],
listRuleValues: (ruleType: string, ruleValue: string, query: object) => [
PROMOTIONS_QUERY_KEY,
ruleType,
@@ -64,19 +71,20 @@ export const usePromotion = (
export const usePromotionRules = (
id: string | null,
ruleType: string,
query?: Record<string, string>,
options?: Omit<
UseQueryOptions<
PromotionRulesListRes,
AdminPromotionRuleListResponse,
Error,
PromotionRulesListRes,
AdminPromotionRuleListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: promotionsQueryKeys.listRules(id, ruleType),
queryFn: async () => client.promotions.listRules(id, ruleType),
queryKey: promotionsQueryKeys.listRules(id, ruleType, query),
queryFn: async () => client.promotions.listRules(id, ruleType, query),
...options,
})
@@ -102,9 +110,9 @@ export const usePromotions = (
export const usePromotionRuleOperators = (
options?: Omit<
UseQueryOptions<
PromotionListRes,
AdminRuleOperatorOptionsListResponse,
Error,
PromotionRuleOperatorsListRes,
AdminRuleOperatorOptionsListResponse,
QueryKey
>,
"queryFn" | "queryKey"
@@ -121,19 +129,21 @@ export const usePromotionRuleOperators = (
export const usePromotionRuleAttributes = (
ruleType: string,
promotionType?: string,
options?: Omit<
UseQueryOptions<
PromotionListRes,
AdminRuleAttributeOptionsListResponse,
Error,
PromotionRuleAttributesListRes,
AdminRuleAttributeOptionsListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: promotionsQueryKeys.listRuleAttributes(ruleType),
queryFn: async () => client.promotions.listRuleAttributes(ruleType),
queryKey: promotionsQueryKeys.listRuleAttributes(ruleType, promotionType),
queryFn: async () =>
client.promotions.listRuleAttributes(ruleType, promotionType),
...options,
})
@@ -207,10 +217,7 @@ export const useUpdatePromotion = (
return useMutation({
mutationFn: (payload) => client.promotions.update(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: promotionsQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all })
options?.onSuccess?.(data, variables, context)
},
@@ -226,10 +233,7 @@ export const usePromotionAddRules = (
return useMutation({
mutationFn: (payload) => client.promotions.addRules(id, ruleType, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: promotionsQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all })
options?.onSuccess?.(data, variables, context)
},
@@ -250,10 +254,7 @@ export const usePromotionRemoveRules = (
mutationFn: (payload) =>
client.promotions.removeRules(id, ruleType, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: promotionsQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all })
options?.onSuccess?.(data, variables, context)
},
@@ -274,13 +275,7 @@ export const usePromotionUpdateRules = (
mutationFn: (payload) =>
client.promotions.updateRules(id, ruleType, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: promotionsQueryKeys.listRules(id, ruleType),
})
queryClient.invalidateQueries({
queryKey: promotionsQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: promotionsQueryKeys.all })
options?.onSuccess?.(data, variables, context)
},

View File

@@ -981,15 +981,15 @@
"conditions": {
"rules": {
"title": "Who can use this code?",
"description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched. Choose between attributes, operators, and values to set up the conditions."
"description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched."
},
"target-rules": {
"title": "What needs to be in the cart to unlock the promotion?",
"description": "If these conditions match, we enable a promotion action on the target items. Choose between attributes, operators, and values to set up the conditions."
"title": "What will the promotion be applied to?",
"description": "The promotion will be applied to items that match the following conditions"
},
"buy-rules": {
"title": "What will the promotion be applied to?",
"description": "The promotion will be applied to items that match these conditions"
"title": "What needs to be in the cart to unlock the promotion?",
"description": "If these conditions match, we enable the promotion on the target items."
}
}
},

View File

@@ -81,15 +81,23 @@ async function removePromotionRules(
)
}
async function listPromotionRules(id: string | null, ruleType: string) {
async function listPromotionRules(
id: string | null,
ruleType: string,
query?: Record<string, string>
) {
return getRequest<PromotionRuleAttributesListRes>(
`/admin/promotions/${id}/${ruleType}`
`/admin/promotions/${id}/${ruleType}`,
query
)
}
async function listPromotionRuleAttributes(ruleType: string) {
async function listPromotionRuleAttributes(
ruleType: string,
promotionType?: string
) {
return getRequest<PromotionRuleAttributesListRes>(
`/admin/promotions/rule-attribute-options/${ruleType}`
`/admin/promotions/rule-attribute-options/${ruleType}?promotion_type=${promotionType}`
)
}

View File

@@ -3,25 +3,23 @@ import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import i18n from "i18next"
import { useState } from "react"
import { useFieldArray, useForm } from "react-hook-form"
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 { getDisguisedRules } from "./utils"
type EditPromotionFormProps = {
promotion: PromotionDTO
rules: PromotionRuleDTO[]
ruleType: RuleTypeValues
attributes: any[]
operators: any[]
handleSubmit: any
isSubmitting: boolean
}
const EditRules = zod.object({
type: zod.string().optional(),
rules: zod.array(
zod.object({
id: zod.string().optional(),
@@ -39,6 +37,7 @@ const EditRules = zod.object({
.min(1, { message: i18n.t("promotions.form.required") }),
]),
required: zod.boolean().optional(),
disguised: zod.boolean().optional(),
field_type: zod.string().optional(),
})
),
@@ -46,41 +45,18 @@ const EditRules = zod.object({
export const EditRulesForm = ({
promotion,
rules,
ruleType,
attributes,
operators,
handleSubmit,
isSubmitting,
}: EditPromotionFormProps) => {
const { t } = useTranslation()
const requiredAttributes = attributes?.filter((ra) => ra.required) || []
const requiredAttributeValues = requiredAttributes?.map((ra) => ra.value)
const disguisedRules =
getDisguisedRules(promotion, requiredAttributes, ruleType) || []
const [rulesToRemove, setRulesToRemove] = useState([])
const form = useForm<zod.infer<typeof EditRules>>({
defaultValues: {
rules: [...disguisedRules, ...rules].map((rule) => ({
id: rule.id,
required: requiredAttributeValues.includes(rule.attribute),
attribute: rule.attribute!,
operator: rule.operator!,
values: Array.isArray(rule?.values)
? rule?.values?.map((v: any) => v.value!)
: rule.values!,
})),
},
defaultValues: { rules: [], type: promotion.type },
resolver: zodResolver(EditRules),
})
const { fields, append, remove, update } = useFieldArray({
control: form.control,
name: "rules",
keyName: "rules_id",
})
const handleFormSubmit = form.handleSubmit(handleSubmit(rulesToRemove))
return (
@@ -90,14 +66,9 @@ export const EditRulesForm = ({
<RulesFormField
form={form}
ruleType={ruleType}
attributes={attributes}
operators={operators}
fields={fields}
setRulesToRemove={setRulesToRemove}
rulesToRemove={rulesToRemove}
appendRule={append}
removeRule={remove}
updateRule={update}
promotionId={promotion.id}
/>
</RouteDrawer.Body>

View File

@@ -1,66 +0,0 @@
import { PromotionDTO } from "@medusajs/types"
import { RuleType } from "../../edit-rules"
// We are disguising couple of database columns as rules here, namely
// apply_to_quantity and buy_rules_min_quantity.
// We need to transform the database value into a disugised "rule" shape
// for the form
export function getDisguisedRules(
promotion: PromotionDTO,
requiredAttributes: any[],
ruleType: string
) {
if (ruleType === RuleType.RULES && !requiredAttributes?.length) {
return []
}
const applyToQuantityRule = requiredAttributes.find(
(attr) => attr.id === "apply_to_quantity"
)
const buyRulesMinQuantityRule = requiredAttributes.find(
(attr) => attr.id === "buy_rules_min_quantity"
)
const currencyCodeRule = requiredAttributes.find(
(attr) => attr.id === "currency_code"
)
if (ruleType === RuleType.RULES) {
return [
{
id: "currency_code",
attribute: "currency_code",
operator: "eq",
required: currencyCodeRule?.required,
values: promotion?.application_method?.currency_code?.toLowerCase(),
},
]
}
if (ruleType === RuleType.TARGET_RULES) {
return [
{
id: "apply_to_quantity",
attribute: "apply_to_quantity",
operator: "eq",
required: applyToQuantityRule?.required,
values: promotion?.application_method?.apply_to_quantity,
},
]
}
if (ruleType === RuleType.BUY_RULES) {
return [
{
id: "buy_rules_min_quantity",
attribute: "buy_rules_min_quantity",
operator: "eq",
required: buyRulesMinQuantityRule?.required,
values: [
{ value: promotion?.application_method?.buy_rules_min_quantity },
],
},
]
}
}

View File

@@ -2,19 +2,19 @@ import {
CreatePromotionRuleDTO,
PromotionDTO,
PromotionRuleDTO,
PromotionRuleOperatorValues,
PromotionRuleResponse,
} from "@medusajs/types"
import { useRouteModal } from "../../../../../../components/route-modal"
import {
usePromotionAddRules,
usePromotionRemoveRules,
usePromotionRuleAttributes,
usePromotionRuleOperators,
usePromotionUpdateRules,
useUpdatePromotion,
} from "../../../../../../hooks/api/promotions"
import { RuleTypeValues } from "../../edit-rules"
import { EditRulesForm } from "../edit-rules-form"
import { getDisguisedRules } from "../edit-rules-form/utils"
import { getRuleValue } from "./utils"
type EditPromotionFormProps = {
promotion: PromotionDTO
@@ -28,26 +28,6 @@ export const EditRulesWrapper = ({
ruleType,
}: EditPromotionFormProps) => {
const { handleSuccess } = useRouteModal()
const {
attributes,
isError: isAttributesError,
error: attributesError,
} = usePromotionRuleAttributes(ruleType!)
const {
operators,
isError: isOperatorsError,
error: operatorsError,
} = usePromotionRuleOperators()
if (isAttributesError || isOperatorsError) {
throw attributesError || operatorsError
}
const requiredAttributes = attributes?.filter((ra) => ra.required) || []
const disguisedRules =
getDisguisedRules(promotion, requiredAttributes, ruleType) || []
const { mutateAsync: updatePromotion } = useUpdatePromotion(promotion.id)
const { mutateAsync: addPromotionRules } = usePromotionAddRules(
promotion.id,
@@ -63,33 +43,21 @@ export const EditRulesWrapper = ({
usePromotionUpdateRules(promotion.id, ruleType)
const handleSubmit = (rulesToRemove?: { id: string }[]) => {
return async function (data: { rules: PromotionRuleDTO[] }) {
return async function (data: { rules: PromotionRuleResponse[] }) {
const applicationMethodData: Record<any, any> = {}
const { rules: allRules = [] } = data
const disguisedRulesData = allRules.filter((rule) =>
disguisedRules.map((rule) => rule.id).includes(rule.id!)
)
const disguisedRules = allRules.filter((rule) => rule.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
// up, abstract this away.
for (const rule of disguisedRulesData) {
const currentAttribute = attributes?.find(
(attr) => attr.value === rule.attribute
)
applicationMethodData[rule.id!] =
currentAttribute?.field_type === "number"
? parseInt(rule.values as unknown as string)
: rule.values
for (const rule of disguisedRules) {
applicationMethodData[rule.attribute] = getRuleValue(rule)
}
// This variable will contain the rules that are actual rule objects, without the disguised
// objects
const rulesData = allRules.filter(
(rule) => !disguisedRules.map((rule) => rule.id).includes(rule.id!)
)
const rulesData = allRules.filter((rule) => !rule.disguised)
const rulesToCreate: CreatePromotionRuleDTO[] = rulesData.filter(
(rule) => !("id" in rule)
)
@@ -121,11 +89,11 @@ export const EditRulesWrapper = ({
rulesToUpdate.length &&
(await updatePromotionRules({
rules: rulesToUpdate.map((rule: PromotionRuleDTO) => {
rules: rulesToUpdate.map((rule: PromotionRuleResponse) => {
return {
id: rule.id!,
attribute: rule.attribute,
operator: rule.operator,
operator: rule.operator as PromotionRuleOperatorValues,
values: rule.values as unknown as string | string[],
}
}),
@@ -135,17 +103,13 @@ export const EditRulesWrapper = ({
}
}
if (attributes && operators) {
return (
<EditRulesForm
promotion={promotion}
rules={rules}
ruleType={ruleType}
attributes={attributes}
operators={operators}
handleSubmit={handleSubmit}
isSubmitting={isPending}
/>
)
}
return (
<EditRulesForm
promotion={promotion}
rules={rules}
ruleType={ruleType}
handleSubmit={handleSubmit}
isSubmitting={isPending}
/>
)
}

View File

@@ -0,0 +1,13 @@
import { PromotionRuleResponse } from "@medusajs/types"
export const getRuleValue = (rule: PromotionRuleResponse) => {
if (rule.field_type === "number") {
return parseInt(rule.values as unknown as string)
}
if (rule.field_type === "select") {
return rule.values[0]
}
return rule.values
}

View File

@@ -0,0 +1,11 @@
export const requiredProductRule = {
id: "product",
attribute: "items.product.id",
attribute_label: "Product",
operator: "eq",
operator_label: "Equal",
values: [],
required: true,
field_type: "select",
disguised: false,
}

View File

@@ -1,31 +1,23 @@
import { XMarkMini } from "@medusajs/icons"
import {
RuleAttributeOptionsResponse,
RuleOperatorOptionsResponse,
} from "@medusajs/types"
import { PromotionRuleResponse } from "@medusajs/types"
import { Badge, Button, Heading, Select, Text } from "@medusajs/ui"
import { Fragment } from "react"
import {
FieldValues,
Path,
UseFieldArrayAppend,
UseFieldArrayRemove,
UseFieldArrayUpdate,
UseFormReturn,
} from "react-hook-form"
import { Fragment, useEffect } from "react"
import { useFieldArray, UseFormReturn, useWatch } from "react-hook-form"
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 { RuleValueFormField } from "../rule-value-form-field"
import { requiredProductRule } from "./constants"
type RulesFormFieldType<TSchema extends FieldValues> = {
form: UseFormReturn<TSchema>
type RulesFormFieldType = {
promotionId?: string
form: UseFormReturn<CreatePromotionSchemaType>
ruleType: "rules" | "target-rules" | "buy-rules"
fields: any[]
attributes: RuleAttributeOptionsResponse[]
operators: RuleOperatorOptionsResponse[]
removeRule: UseFieldArrayRemove
updateRule: UseFieldArrayUpdate<TSchema>
appendRule: UseFieldArrayAppend<TSchema>
setRulesToRemove?: any
rulesToRemove?: any
scope?:
@@ -34,20 +26,91 @@ type RulesFormFieldType<TSchema extends FieldValues> = {
| "application_method.target_rules"
}
export const RulesFormField = <TSchema extends FieldValues>({
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,
fields,
attributes,
operators,
removeRule,
updateRule,
appendRule,
setRulesToRemove,
rulesToRemove,
scope = "rules",
}: RulesFormFieldType<TSchema>) => {
promotionId,
}: 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,
name: scope,
keyName: scope,
})
const promotionType: string = useWatch({
control: form.control,
name: "type",
})
const query: Record<string, string> = promotionType
? { promotion_type: promotionType }
: {}
const { rules, isLoading } = usePromotionRules(
promotionId || null,
ruleType,
query,
{
enabled: !!promotionType,
}
)
useEffect(() => {
if (isLoading) {
return
}
if (ruleType === "rules" && !fields.length) {
replace(generateRuleAttributes(rules) as any)
}
if (ruleType === "rules" && promotionType === "standard") {
form.resetField("application_method.buy_rules")
form.resetField("application_method.target_rules")
}
if (
["buy-rules", "target-rules"].includes(ruleType) &&
promotionType === "standard"
) {
form.resetField(scope)
replace([])
}
if (
["buy-rules", "target-rules"].includes(ruleType) &&
promotionType === "buyget"
) {
form.resetField(scope)
const rulesToAppend = promotionId
? rules
: [...rules, requiredProductRule]
replace(generateRuleAttributes(rulesToAppend) as any)
}
}, [promotionType, isLoading])
return (
<div className="flex flex-col">
@@ -62,17 +125,17 @@ export const RulesFormField = <TSchema extends FieldValues>({
{fields.map((fieldRule: any, index) => {
const identifier = fieldRule.id
const { ref: attributeRef, ...attributeField } = form.register(
`${scope}.${index}.attribute` as Path<TSchema>
`${scope}.${index}.attribute`
)
const { ref: operatorRef, ...operatorsField } = form.register(
`${scope}.${index}.operator` as Path<TSchema>
`${scope}.${index}.operator`
)
const { ref: valuesRef, ...valuesField } = form.register(
`${scope}.${index}.values` as Path<TSchema>
`${scope}.${index}.values`
)
return (
<Fragment key={`${fieldRule.id}.${index}`}>
<Fragment key={`${fieldRule.id}.${index}.${fieldRule.attribute}`}>
<div className="bg-ui-bg-subtle border-ui-border-base flex flex-row gap-2 rounded-xl border px-2 py-2">
<div className="grow">
<Form.Field
@@ -102,10 +165,10 @@ export const RulesFormField = <TSchema extends FieldValues>({
<Select
{...field}
onValueChange={(e) => {
updateRule(index, { ...fieldRule, values: [] })
update(index, { ...fieldRule, values: [] })
onChange(e)
}}
disabled={fieldRule.required}
disabled={fieldRule.disguised}
>
<Select.Trigger
ref={attributeRef}
@@ -149,7 +212,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
<Select
{...field}
onValueChange={onChange}
disabled={fieldRule.required}
disabled={fieldRule.disguised}
>
<Select.Trigger
ref={operatorRef}
@@ -203,7 +266,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
setRulesToRemove &&
setRulesToRemove([...rulesToRemove, fieldRule])
removeRule(index)
remove(index)
}
}}
/>
@@ -229,7 +292,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
variant="secondary"
className="inline-block"
onClick={() => {
appendRule({
append({
attribute: "",
operator: "",
values: [],
@@ -251,7 +314,7 @@ export const RulesFormField = <TSchema extends FieldValues>({
setRulesToRemove &&
setRulesToRemove(fields.filter((field: any) => !field.required))
removeRule(indicesToRemove)
remove(indicesToRemove)
}}
>
{t("promotions.fields.clearAll")}

View File

@@ -11,16 +11,11 @@ import {
Text,
} from "@medusajs/ui"
import { useEffect, useMemo, useState } from "react"
import { useFieldArray, useForm, useWatch } from "react-hook-form"
import { useForm, useWatch } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import { z } from "zod"
import {
PromotionRuleOperatorValues,
PromotionRuleResponse,
RuleAttributeOptionsResponse,
RuleOperatorOptionsResponse,
} from "@medusajs/types"
import { PromotionRuleOperatorValues } from "@medusajs/types"
import { Divider } from "../../../../../components/common/divider"
import { Form } from "../../../../../components/common/form"
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
@@ -38,42 +33,13 @@ import { Tab } from "./constants"
import { CreatePromotionSchema } from "./form-schema"
import { templates } from "./templates"
type CreatePromotionFormProps = {
ruleAttributes: RuleAttributeOptionsResponse[]
targetRuleAttributes: RuleAttributeOptionsResponse[]
buyRuleAttributes: RuleAttributeOptionsResponse[]
operators: RuleOperatorOptionsResponse[]
rules: PromotionRuleResponse[]
targetRules: PromotionRuleResponse[]
buyRules: PromotionRuleResponse[]
}
export const CreatePromotionForm = ({
ruleAttributes,
targetRuleAttributes,
buyRuleAttributes,
operators,
rules,
targetRules,
buyRules,
}: CreatePromotionFormProps) => {
export const CreatePromotionForm = () => {
const [tab, setTab] = useState<Tab>(Tab.TYPE)
const [detailsValidated, setDetailsValidated] = useState(false)
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
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?.values?.map((v: { value: string }) => v.value!),
}))
const form = useForm<z.infer<typeof CreatePromotionSchema>>({
defaultValues: {
campaign_id: undefined,
@@ -82,53 +48,20 @@ export const CreatePromotionForm = ({
is_automatic: "false",
code: "",
type: "standard",
rules: generateRuleAttributes(rules),
rules: [],
application_method: {
allocation: "each",
type: "fixed",
target_type: "items",
max_quantity: 1,
target_rules: generateRuleAttributes(targetRules),
buy_rules: generateRuleAttributes(buyRules),
target_rules: [],
buy_rules: [],
},
campaign: undefined,
},
resolver: zodResolver(CreatePromotionSchema),
})
const {
fields: ruleFields,
append: appendRule,
remove: removeRule,
update: updateRule,
} = useFieldArray({
control: form.control,
name: "rules",
keyName: "rules_id",
})
const {
fields: targetRuleFields,
append: appendTargetRule,
remove: removeTargetRule,
update: updateTargetRule,
} = useFieldArray({
control: form.control,
name: "application_method.target_rules",
keyName: "target_rules_id",
})
const {
fields: buyRuleFields,
append: appendBuyRule,
remove: removeBuyRule,
update: updateBuyRule,
} = useFieldArray({
control: form.control,
name: "application_method.buy_rules",
keyName: "buy_rules_id",
})
const { mutateAsync: createPromotion } = useCreatePromotion()
const handleSubmit = form.handleSubmit(
@@ -265,14 +198,11 @@ export const CreatePromotionForm = ({
})
const isFixedValueType = watchValueType === "fixed"
const watchAllocation = useWatch({
control: form.control,
name: "application_method.allocation",
})
const isAllocationEach = watchAllocation === "each"
useEffect(() => {
if (watchAllocation === "across") {
form.setValue("application_method.max_quantity", null)
@@ -296,17 +226,6 @@ export const CreatePromotionForm = ({
const { campaigns } = useCampaigns(campaignQuery)
useEffect(() => {
if (isTypeStandard) {
form.setValue("application_method.buy_rules", undefined)
} else {
form.setValue(
"application_method.buy_rules",
generateRuleAttributes(buyRules)
)
}
}, [isTypeStandard])
const detailsProgress = useMemo(() => {
if (detailsValidated) {
return "completed"
@@ -592,16 +511,7 @@ export const CreatePromotionForm = ({
<Divider />
<RulesFormField
form={form}
ruleType={"rules"}
attributes={ruleAttributes}
operators={operators}
fields={ruleFields}
appendRule={appendRule}
removeRule={removeRule}
updateRule={updateRule}
/>
<RulesFormField form={form} ruleType={"rules"} />
<Divider />
@@ -756,7 +666,7 @@ export const CreatePromotionForm = ({
/>
)}
{isTypeStandard && isAllocationEach && (
{isTypeStandard && watchAllocation === "each" && (
<div className="flex gap-y-4">
<Form.Field
control={form.control}
@@ -800,33 +710,23 @@ export const CreatePromotionForm = ({
<Divider />
{!isTypeStandard && (
<>
<RulesFormField
form={form}
ruleType={"buy-rules"}
scope="application_method.buy_rules"
/>
<Divider />
</>
)}
<RulesFormField
form={form}
ruleType={"target-rules"}
attributes={targetRuleAttributes}
operators={operators}
fields={targetRuleFields}
appendRule={appendTargetRule}
removeRule={removeTargetRule}
updateRule={updateTargetRule}
scope="application_method.target_rules"
/>
<Divider />
{!isTypeStandard && (
<RulesFormField
form={form}
ruleType={"buy-rules"}
attributes={buyRuleAttributes}
operators={operators}
fields={buyRuleFields}
appendRule={appendBuyRule}
removeRule={removeBuyRule}
updateRule={updateBuyRule}
scope="application_method.buy_rules"
/>
)}
</ProgressTabs.Content>
<ProgressTabs.Content

View File

@@ -32,7 +32,7 @@ export const CreatePromotionSchema = z
currency_code: z.string(),
max_quantity: z.number().optional().nullable(),
target_rules: RuleSchema,
buy_rules: RuleSchema.min(2).optional(),
buy_rules: RuleSchema,
type: z.enum(["fixed", "percentage"]),
target_type: z.enum(["order", "shipping_methods", "items"]),
}),

View File

@@ -1,42 +1,6 @@
import { RouteFocusModal } from "../../../components/route-modal"
import {
usePromotionRuleAttributes,
usePromotionRuleOperators,
usePromotionRules,
} from "../../../hooks/api/promotions"
import { CreatePromotionForm } from "./components/create-promotion-form/create-promotion-form"
export const PromotionCreate = () => {
const { attributes: ruleAttributes } = usePromotionRuleAttributes("rules")
const { attributes: targetRuleAttributes } =
usePromotionRuleAttributes("target-rules")
const { attributes: buyRuleAttributes } =
usePromotionRuleAttributes("buy-rules")
const { rules } = usePromotionRules(null, "rules")
const { rules: targetRules } = usePromotionRules(null, "target-rules")
const { rules: buyRules } = usePromotionRules(null, "buy-rules")
const { operators } = usePromotionRuleOperators()
return (
<RouteFocusModal>
{rules &&
buyRules &&
targetRules &&
operators &&
ruleAttributes &&
targetRuleAttributes &&
buyRuleAttributes && (
<CreatePromotionForm
ruleAttributes={ruleAttributes}
targetRuleAttributes={targetRuleAttributes}
buyRuleAttributes={buyRuleAttributes}
operators={operators}
rules={rules}
targetRules={targetRules}
buyRules={buyRules}
/>
)}
</RouteFocusModal>
)
return <RouteFocusModal>{<CreatePromotionForm />}</RouteFocusModal>
}

View File

@@ -30,7 +30,11 @@ function RuleBlock({ rule }: RuleProps) {
<BadgeListSummary
inline
className="!txt-compact-small-plus"
list={rule.values.map((v) => v.label)}
list={
rule.field_type === "number"
? [rule.values]
: rule.values?.map((v) => v.label)
}
/>
</div>
</div>

View File

@@ -17,9 +17,15 @@ export const PromotionDetail = () => {
const { id } = useParams()
const { promotion, isLoading } = usePromotion(id!, { initialData })
const { rules } = usePromotionRules(id!, "rules")
const { rules: targetRules } = usePromotionRules(id!, "target-rules")
const { rules: buyRules } = usePromotionRules(id!, "buy-rules")
const query: Record<string, string> = {}
if (promotion?.type === "buyget") {
query.promotion_type = promotion.type
}
const { rules } = usePromotionRules(id!, "rules", query)
const { rules: targetRules } = usePromotionRules(id!, "target-rules", query)
const { rules: buyRules } = usePromotionRules(id!, "buy-rules", query)
if (isLoading || !promotion) {
return <div>Loading...</div>

View File

@@ -11,5 +11,5 @@ export interface PromotionRuleResponse {
}
export interface AdminPromotionRuleListResponse {
attributes: PromotionRuleResponse[]
rules: PromotionRuleResponse[]
}

View File

@@ -13,6 +13,11 @@ import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
*/
export type PromotionTypeValues = "standard" | "buyget"
/**
* The promotion's possible rule types.
*/
export type RuleTypeValues = "rules" | "buy-rules" | "target-rules"
/**
* The promotion details.
*/

View File

@@ -10,8 +10,8 @@ import {
MedusaResponse,
} from "../../../../../types/routing"
import {
getRuleAttributesMap,
operatorsMap,
ruleAttributesMap,
ruleQueryConfigurations,
validateRuleType,
} from "../../utils"
@@ -33,7 +33,9 @@ export const GET = async (
})
const [promotion] = await remoteQuery(queryObject)
const ruleAttributes = ruleAttributesMap[ruleType]
const ruleAttributes = getRuleAttributesMap(
promotion?.type || req.query.promotion_type
)[ruleType]
const promotionRules: any[] = []
if (dasherizedRuleType === RuleType.RULES) {
@@ -49,8 +51,19 @@ export const GET = async (
const requiredRules = ruleAttributes.filter((attr) => !!attr.required)
for (const disguisedRule of disguisedRules) {
const value = promotion?.application_method?.[disguisedRule.id]
const values = value ? [{ label: value, value }] : []
const getValues = () => {
const value = promotion?.application_method?.[disguisedRule.id]
if (disguisedRule.field_type === "number") {
return value
}
if (value) {
return [{ label: value, value }]
}
return []
}
transformedRules.push({
id: undefined,
@@ -60,7 +73,7 @@ export const GET = async (
hydrate: disguisedRule.hydrate || false,
operator: RuleOperator.EQ,
operator_label: operatorsMap[RuleOperator.EQ].label,
values,
values: getValues(),
disguised: true,
required: true,
})
@@ -90,7 +103,7 @@ export const GET = async (
entryPoint: queryConfig.entryPoint,
variables: {
filters: {
[queryConfig.valueAttr]: promotionRule.values.map((v) => v.value),
[queryConfig.valueAttr]: promotionRule.values?.map((v) => v.value),
},
},
fields: [queryConfig.labelAttr, queryConfig.valueAttr],
@@ -104,10 +117,11 @@ export const GET = async (
])
)
promotionRule.values = promotionRule.values.map((value) => ({
value: value.value,
label: valueLabelMap.get(value.value) || value.value,
}))
promotionRule.values =
promotionRule.values?.map((value) => ({
value: value.value,
label: valueLabelMap.get(value.value) || value.value,
})) || promotionRule.values
if (!currentRuleAttribute.hydrate) {
transformedRules.push({

View File

@@ -1,4 +1,5 @@
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { unlessPath } from "../../utils/unless-path"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import { createBatchBody } from "../../utils/validators"
@@ -62,9 +63,12 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"],
matcher: "/admin/promotions/:id/:rule_type",
middlewares: [
validateAndTransformQuery(
AdminGetPromotionRuleTypeParams,
QueryConfig.retrieveTransformQueryConfig
unlessPath(
/.*\/promotions\/rule-attribute-options/,
validateAndTransformQuery(
AdminGetPromotionRuleTypeParams,
QueryConfig.retrieveTransformQueryConfig
)
),
],
},
@@ -118,4 +122,14 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["GET"],
matcher: "/admin/promotions/rule-attribute-options/:rule_type",
middlewares: [
validateAndTransformQuery(
AdminGetPromotionRuleParams,
QueryConfig.listRuleTransformQueryConfig
),
],
},
]

View File

@@ -2,17 +2,19 @@ import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { ruleAttributesMap, validateRuleType } from "../../utils"
import { getRuleAttributesMap, validateRuleType } from "../../utils"
import { AdminGetPromotionRuleParamsType } from "../../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
req: AuthenticatedMedusaRequest<AdminGetPromotionRuleParamsType>,
res: MedusaResponse
) => {
const { rule_type: ruleType } = req.params
validateRuleType(ruleType)
const attributes = ruleAttributesMap[ruleType] || []
const attributes =
getRuleAttributesMap(req.query.promotion_type as string)[ruleType] || []
res.json({
attributes,

View File

@@ -11,12 +11,17 @@ import {
validateRuleAttribute,
validateRuleType,
} from "../../../utils"
import { AdminGetPromotionRuleParamsType } from "../../../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
req: AuthenticatedMedusaRequest<AdminGetPromotionRuleParamsType>,
res: MedusaResponse
) => {
const { rule_type: ruleType, rule_attribute_id: ruleAttributeId } = req.params
const {
rule_type: ruleType,
rule_attribute_id: ruleAttributeId,
promotion_type: promotionType,
} = req.params
const queryConfig = ruleQueryConfigurations[ruleAttributeId]
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const filterableFields = req.filterableFields
@@ -28,7 +33,7 @@ export const GET = async (
}
validateRuleType(ruleType)
validateRuleAttribute(ruleType, ruleAttributeId)
validateRuleAttribute(promotionType, ruleType, ruleAttributeId)
const { rows } = await remoteQuery(
remoteQueryObjectFromString({

View File

@@ -1,21 +1,11 @@
import { PromotionType } from "@medusajs/utils"
export enum DisguisedRule {
APPLY_TO_QUANTITY = "apply_to_quantity",
BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity",
CURRENCY_CODE = "currency_code",
}
export const disguisedRulesMap = {
[DisguisedRule.APPLY_TO_QUANTITY]: {
relation: "application_method",
},
[DisguisedRule.BUY_RULES_MIN_QUANTITY]: {
relation: "application_method",
},
[DisguisedRule.CURRENCY_CODE]: {
relation: "application_method",
},
}
const ruleAttributes = [
{
id: DisguisedRule.CURRENCY_CODE,
@@ -94,7 +84,7 @@ const commonAttributes = [
},
]
const buyRuleAttributes = [
const buyGetBuyRules = [
{
id: DisguisedRule.BUY_RULES_MIN_QUANTITY,
value: DisguisedRule.BUY_RULES_MIN_QUANTITY,
@@ -103,10 +93,9 @@ const buyRuleAttributes = [
required: true,
disguised: true,
},
...commonAttributes,
]
const targetRuleAttributes = [
const buyGetTargetRules = [
{
id: DisguisedRule.APPLY_TO_QUANTITY,
value: DisguisedRule.APPLY_TO_QUANTITY,
@@ -115,11 +104,19 @@ const targetRuleAttributes = [
required: true,
disguised: true,
},
...commonAttributes,
]
export const ruleAttributesMap = {
rules: ruleAttributes,
"target-rules": targetRuleAttributes,
"buy-rules": buyRuleAttributes,
export const getRuleAttributesMap = (promotionType?: string) => {
const map = {
rules: [...ruleAttributes],
"target-rules": [...commonAttributes],
"buy-rules": [...commonAttributes],
}
if (promotionType === PromotionType.BUYGET) {
map["buy-rules"].push(...buyGetBuyRules)
map["target-rules"].push(...buyGetTargetRules)
}
return map
}

View File

@@ -1,11 +1,12 @@
import { MedusaError } from "@medusajs/utils"
import { ruleAttributesMap } from "./rule-attributes-map"
import { getRuleAttributesMap } from "./rule-attributes-map"
export function validateRuleAttribute(
promotionType: string | undefined,
ruleType: string,
ruleAttributeId: string
) {
const ruleAttributes = ruleAttributesMap[ruleType] || []
const ruleAttributes = getRuleAttributesMap(promotionType)[ruleType] || []
const ruleAttribute = ruleAttributes.find((obj) => obj.id === ruleAttributeId)
if (!ruleAttribute) {

View File

@@ -47,12 +47,18 @@ export const AdminGetPromotionsParams = createFindParams({
export type AdminGetPromotionRuleParamsType = z.infer<
typeof AdminGetPromotionRuleParams
>
export const AdminGetPromotionRuleParams = createSelectParams()
export const AdminGetPromotionRuleParams = z.object({
promotion_type: z.string().optional(),
})
export type AdminGetPromotionRuleTypeParamsType = z.infer<
typeof AdminGetPromotionRuleTypeParams
>
export const AdminGetPromotionRuleTypeParams = createSelectParams()
export const AdminGetPromotionRuleTypeParams = createSelectParams().merge(
z.object({
promotion_type: z.string().optional(),
})
)
export type AdminGetPromotionsRuleValueParamsType = z.infer<
typeof AdminGetPromotionsRuleValueParams