feat(dashboard,core,modules): free shipping promotion in dashboard (#13263)
* feat(dashboard,core,modules): free shipping promotion in dashboard * self-review * adapt for edit to work * changeset * integration tests * across for each * remove only from tests * remove console log * revert to across * update wording for shipping promotions * modify changeset * suggestion frane * fix i18n schema
This commit is contained in:
1 parent
92e3b2b432
commit
486621383a
22 files changed
+417
-274
No files matched your search
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@medusajs/link-modules": patch
|
||||
"@medusajs/promotion": patch
|
||||
"@medusajs/dashboard": patch
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/js-sdk": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat(dashboard,core-flows,js-sdk,link-modules,promotion): free shipping promotion in dashboard
|
||||
@@ -21,10 +21,15 @@ export const promotionsQueryKeys = {
|
||||
ruleType: string,
|
||||
query?: HttpTypes.AdminGetPromotionRuleParams
|
||||
) => [PROMOTIONS_QUERY_KEY, id, ruleType, query],
|
||||
listRuleAttributes: (ruleType: string, promotionType?: string) => [
|
||||
listRuleAttributes: (
|
||||
ruleType: string,
|
||||
promotionType?: string,
|
||||
applicationMethodTargetType?: string
|
||||
) => [
|
||||
PROMOTIONS_QUERY_KEY,
|
||||
ruleType,
|
||||
promotionType,
|
||||
applicationMethodTargetType,
|
||||
],
|
||||
listRuleValues: (
|
||||
ruleType: string,
|
||||
@@ -101,6 +106,7 @@ export const usePromotions = (
|
||||
export const usePromotionRuleAttributes = (
|
||||
ruleType: string,
|
||||
promotionType?: string,
|
||||
applicationMethodTargetType?: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.AdminRuleAttributeOptionsListResponse,
|
||||
@@ -112,9 +118,17 @@ export const usePromotionRuleAttributes = (
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.listRuleAttributes(ruleType, promotionType),
|
||||
queryKey: promotionsQueryKeys.listRuleAttributes(
|
||||
ruleType,
|
||||
promotionType,
|
||||
applicationMethodTargetType
|
||||
),
|
||||
queryFn: async () =>
|
||||
sdk.admin.promotion.listRuleAttributes(ruleType, promotionType),
|
||||
sdk.admin.promotion.listRuleAttributes(
|
||||
ruleType,
|
||||
promotionType,
|
||||
applicationMethodTargetType
|
||||
),
|
||||
...options,
|
||||
})
|
||||
|
||||
|
||||
@@ -7516,14 +7516,47 @@
|
||||
"target-rules": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
"order": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
"shipping_methods": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"],
|
||||
"required": ["order", "shipping_methods", "items"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"buy-rules": {
|
||||
|
||||
@@ -2010,8 +2010,18 @@
|
||||
"description": "Which customer is allowed to use the promotion code? Promotion code can be used by all customers if left untouched."
|
||||
},
|
||||
"target-rules": {
|
||||
"title": "What items will the promotion be applied to?",
|
||||
"description": "The promotion will be applied to items that match the following conditions."
|
||||
"order": {
|
||||
"title": "What items will the promotion be applied to?",
|
||||
"description": "The promotion will be applied to items that match the following conditions."
|
||||
},
|
||||
"shipping_methods": {
|
||||
"title": "What shipping methods will the promotion be applied to?",
|
||||
"description": "The promotion will be applied to shipping methods that match the following conditions."
|
||||
},
|
||||
"items": {
|
||||
"title": "What items will the promotion be applied to?",
|
||||
"description": "The promotion will be applied to items that match the following conditions."
|
||||
}
|
||||
},
|
||||
"buy-rules": {
|
||||
"title": "What needs to be in the cart to unlock the promotion?",
|
||||
|
||||
+7
-1
@@ -28,7 +28,13 @@ export const EditRulesForm = ({
|
||||
const [rulesToRemove, setRulesToRemove] = useState([])
|
||||
|
||||
const form = useForm<EditRulesType>({
|
||||
defaultValues: { rules: [], type: promotion.type },
|
||||
defaultValues: {
|
||||
rules: [],
|
||||
type: promotion.type,
|
||||
application_method: {
|
||||
target_type: promotion.application_method?.target_type,
|
||||
},
|
||||
},
|
||||
resolver: zodResolver(EditRules),
|
||||
})
|
||||
|
||||
|
||||
+3
@@ -24,6 +24,9 @@ export const EditRules = z.object({
|
||||
field_type: z.string().optional(),
|
||||
})
|
||||
),
|
||||
application_method: z.object({
|
||||
target_type: z.enum(["order", "shipping_methods", "items"]),
|
||||
}),
|
||||
})
|
||||
|
||||
export type EditRulesType = z.infer<typeof EditRules>
|
||||
+10
-3
@@ -1,9 +1,13 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
ApplicationMethodTargetTypeValues,
|
||||
HttpTypes,
|
||||
RuleTypeValues,
|
||||
} from "@medusajs/types"
|
||||
import { Input } from "@medusajs/ui"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||
import { useStore } from "../../../../../../hooks/api/store"
|
||||
import { useStore } from "../../../../../../hooks/api"
|
||||
import { useComboboxData } from "../../../../../../hooks/use-combobox-data"
|
||||
import { sdk } from "../../../../../../lib/client"
|
||||
|
||||
@@ -18,7 +22,8 @@ type RuleValueFormFieldType = {
|
||||
operator: string
|
||||
fieldRule: any
|
||||
attributes: HttpTypes.AdminRuleAttributeOption[]
|
||||
ruleType: "rules" | "target-rules" | "buy-rules"
|
||||
ruleType: RuleTypeValues
|
||||
applicationMethodTargetType: ApplicationMethodTargetTypeValues | undefined
|
||||
}
|
||||
|
||||
const buildFilters = (attribute?: string, store?: HttpTypes.AdminStore) => {
|
||||
@@ -44,6 +49,7 @@ export const RuleValueFormField = ({
|
||||
fieldRule,
|
||||
attributes,
|
||||
ruleType,
|
||||
applicationMethodTargetType,
|
||||
}: RuleValueFormFieldType) => {
|
||||
const attribute = attributes?.find(
|
||||
(attr) => attr.value === fieldRule.attribute
|
||||
@@ -59,6 +65,7 @@ export const RuleValueFormField = ({
|
||||
{
|
||||
...params,
|
||||
...buildFilters(attribute?.id, store!),
|
||||
application_method_target_type: applicationMethodTargetType,
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
+23
-3
@@ -41,7 +41,11 @@ export const RulesFormField = ({
|
||||
}: RulesFormFieldType) => {
|
||||
const { t } = useTranslation()
|
||||
const formData = form.getValues()
|
||||
const { attributes } = usePromotionRuleAttributes(ruleType, formData.type)
|
||||
const { attributes } = usePromotionRuleAttributes(
|
||||
ruleType,
|
||||
formData.type,
|
||||
formData.application_method?.target_type
|
||||
)
|
||||
|
||||
const { fields, append, remove, update, replace } = useFieldArray({
|
||||
control: form.control,
|
||||
@@ -61,10 +65,17 @@ export const RulesFormField = ({
|
||||
defaultValue: promotion?.application_method?.type,
|
||||
})
|
||||
|
||||
const applicationMethodTargetType = useWatch({
|
||||
control: form.control,
|
||||
name: "application_method.target_type",
|
||||
defaultValue: promotion?.application_method?.target_type,
|
||||
})
|
||||
|
||||
const query: Record<string, string> = promotionType
|
||||
? {
|
||||
promotion_type: promotionType,
|
||||
application_method_type: applicationMethodType,
|
||||
application_method_target_type: applicationMethodTargetType,
|
||||
}
|
||||
: {}
|
||||
|
||||
@@ -121,11 +132,19 @@ export const RulesFormField = ({
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Heading level="h2" className="mb-2">
|
||||
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||
{t(
|
||||
ruleType === "target-rules"
|
||||
? `promotions.fields.conditions.${ruleType}.${applicationMethodTargetType}.title`
|
||||
: `promotions.fields.conditions.${ruleType}.title`
|
||||
)}
|
||||
</Heading>
|
||||
|
||||
<Text className="text-ui-fg-subtle txt-small mb-6">
|
||||
{t(`promotions.fields.conditions.${ruleType}.description`)}
|
||||
{t(
|
||||
ruleType === "target-rules"
|
||||
? `promotions.fields.conditions.${ruleType}.${applicationMethodTargetType}.description`
|
||||
: `promotions.fields.conditions.${ruleType}.description`
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{fields.map((fieldRule, index) => {
|
||||
@@ -307,6 +326,7 @@ export const RulesFormField = ({
|
||||
fieldRule={fieldRule}
|
||||
attributes={attributes}
|
||||
ruleType={ruleType}
|
||||
applicationMethodTargetType={applicationMethodTargetType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+90
-87
@@ -678,60 +678,62 @@ export const CreatePromotionForm = () => {
|
||||
|
||||
<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")}
|
||||
/>
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<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-x-2 gap-y-4">
|
||||
{!currentTemplate?.hiddenFields?.includes(
|
||||
"application_method.value"
|
||||
) && (
|
||||
{!currentTemplate?.hiddenFields?.includes(
|
||||
"application_method.value"
|
||||
) && (
|
||||
<>
|
||||
<Divider />
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.value"
|
||||
@@ -806,48 +808,48 @@ export const CreatePromotionForm = () => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isTypeStandard && watchAllocation === "each" && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.max_quantity"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Label>
|
||||
{t("promotions.form.max_quantity.title")}
|
||||
</Form.Label>
|
||||
{isTypeStandard && watchAllocation === "each" && (
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="application_method.max_quantity"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Label>
|
||||
{t("promotions.form.max_quantity.title")}
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...form.register(
|
||||
"application_method.max_quantity",
|
||||
{ valueAsNumber: true }
|
||||
)}
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="3"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...form.register(
|
||||
"application_method.max_quantity",
|
||||
{ valueAsNumber: true }
|
||||
)}
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="3"
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="promotions.form.max_quantity.description"
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="promotions.form.max_quantity.description"
|
||||
components={[<br key="break" />]}
|
||||
/>
|
||||
</Text>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTypeStandard &&
|
||||
!currentTemplate?.hiddenFields?.includes(
|
||||
@@ -901,6 +903,7 @@ export const CreatePromotionForm = () => {
|
||||
|
||||
{!isTypeStandard && (
|
||||
<>
|
||||
<Divider />
|
||||
<RulesFormField
|
||||
form={form}
|
||||
ruleType={"buy-rules"}
|
||||
|
||||
+23
@@ -22,6 +22,12 @@ const buyGetHiddenFields = [
|
||||
"is_tax_inclusive",
|
||||
]
|
||||
|
||||
const freeShippingHiddenFields = [
|
||||
...commonHiddenFields,
|
||||
"application_method.value",
|
||||
"is_tax_inclusive",
|
||||
]
|
||||
|
||||
export const templates = [
|
||||
{
|
||||
id: "amount_off_products",
|
||||
@@ -104,4 +110,21 @@ export const templates = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "shipping_discount",
|
||||
type: "standard",
|
||||
title: "Free shipping",
|
||||
description: "Applies a 100% discount to shipping fees",
|
||||
hiddenFields: freeShippingHiddenFields,
|
||||
defaults: {
|
||||
is_automatic: "false",
|
||||
type: "standard",
|
||||
application_method: {
|
||||
allocation: "across",
|
||||
target_type: "shipping_methods",
|
||||
type: "percentage",
|
||||
value: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
+8
-2
@@ -1,5 +1,5 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { HttpTypes, PromotionRuleTypes } from "@medusajs/types"
|
||||
import { ApplicationMethodTargetTypeValues, HttpTypes, PromotionRuleTypes, } from "@medusajs/types"
|
||||
import { Badge, Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
@@ -44,11 +44,13 @@ function RuleBlock({ rule }: RuleProps) {
|
||||
type PromotionConditionsSectionProps = {
|
||||
rules: HttpTypes.AdminPromotionRule[]
|
||||
ruleType: PromotionRuleTypes
|
||||
applicationMethodTargetType: ApplicationMethodTargetTypeValues
|
||||
}
|
||||
|
||||
export const PromotionConditionsSection = ({
|
||||
rules,
|
||||
ruleType,
|
||||
applicationMethodTargetType,
|
||||
}: PromotionConditionsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -57,7 +59,11 @@ export const PromotionConditionsSection = ({
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<Heading>
|
||||
{t(`promotions.fields.conditions.${ruleType}.title`)}
|
||||
{t(
|
||||
ruleType === "target-rules"
|
||||
? `promotions.fields.conditions.${ruleType}.${applicationMethodTargetType}.title`
|
||||
: `promotions.fields.conditions.${ruleType}.title`
|
||||
)}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -52,11 +52,15 @@ export const PromotionDetail = () => {
|
||||
<PromotionConditionsSection
|
||||
rules={targetRules || []}
|
||||
ruleType={"target-rules"}
|
||||
applicationMethodTargetType={
|
||||
promotion.application_method.target_type || "items"
|
||||
}
|
||||
/>
|
||||
{promotion.type === "buyget" && (
|
||||
<PromotionConditionsSection
|
||||
rules={buyRules || []}
|
||||
ruleType={"buy-rules"}
|
||||
applicationMethodTargetType={"items"}
|
||||
/>
|
||||
)}
|
||||
</TwoColumnPage.Main>
|
||||
|
||||
+132
-126
@@ -1,13 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { AdminPromotion } from "@medusajs/types"
|
||||
import {
|
||||
Button,
|
||||
CurrencyInput,
|
||||
Input,
|
||||
RadioGroup,
|
||||
Switch,
|
||||
Text,
|
||||
} from "@medusajs/ui"
|
||||
import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import { useEffect } from "react"
|
||||
@@ -33,6 +26,7 @@ const EditPromotionSchema = zod.object({
|
||||
value_type: zod.enum(["fixed", "percentage"]),
|
||||
value: zod.number(),
|
||||
allocation: zod.enum(["each", "across"]),
|
||||
target_type: zod.enum(["order", "shipping_methods", "items"]),
|
||||
})
|
||||
|
||||
export const EditPromotionDetailsForm = ({
|
||||
@@ -50,6 +44,7 @@ export const EditPromotionDetailsForm = ({
|
||||
value: promotion.application_method!.value,
|
||||
allocation: promotion.application_method!.allocation,
|
||||
value_type: promotion.application_method!.type,
|
||||
target_type: promotion.application_method!.target_type,
|
||||
},
|
||||
resolver: zodResolver(EditPromotionSchema),
|
||||
})
|
||||
@@ -223,128 +218,139 @@ export const EditPromotionDetailsForm = ({
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value_type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.fields.value_type")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
)}
|
||||
/>
|
||||
{promotion.application_method?.target_type !==
|
||||
"shipping_methods" && (
|
||||
<>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value_type"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.fields.value_type")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"percentage"}
|
||||
label={t(
|
||||
"promotions.form.value_type.percentage.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.value_type.percentage.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"percentage"}
|
||||
label={t(
|
||||
"promotions.form.value_type.percentage.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.value_type.percentage.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
const currencyCode =
|
||||
promotion.application_method?.currency_code ?? "USD"
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
const currencyCode =
|
||||
promotion.application_method?.currency_code ?? "USD"
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{isFixedValueType
|
||||
? t("fields.amount")
|
||||
: t("fields.percentage")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
{isFixedValueType ? (
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
onValueChange={(val) =>
|
||||
onChange(val ? parseInt(val) : null)
|
||||
}
|
||||
code={currencyCode}
|
||||
symbol={getCurrencySymbol(currencyCode)}
|
||||
{...field}
|
||||
value={field.value}
|
||||
/>
|
||||
) : (
|
||||
<DeprecatedPercentageInput
|
||||
key="amount"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e.target.value === ""
|
||||
? null
|
||||
: parseInt(e.target.value)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="allocation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("promotions.fields.allocation")}</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{isFixedValueType
|
||||
? t("fields.amount")
|
||||
: t("fields.percentage")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
{isFixedValueType ? (
|
||||
<CurrencyInput
|
||||
min={0}
|
||||
onValueChange={(val) =>
|
||||
onChange(val ? parseInt(val) : null)
|
||||
}
|
||||
code={currencyCode}
|
||||
symbol={getCurrencySymbol(currencyCode)}
|
||||
{...field}
|
||||
value={field.value}
|
||||
/>
|
||||
) : (
|
||||
<DeprecatedPercentageInput
|
||||
key="amount"
|
||||
min={0}
|
||||
max={100}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e.target.value === ""
|
||||
? null
|
||||
: parseInt(e.target.value)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="allocation"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("promotions.fields.allocation")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<RadioGroup
|
||||
className="flex-col 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"
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"across"}
|
||||
label={t("promotions.form.allocation.across.title")}
|
||||
description={t(
|
||||
"promotions.form.allocation.across.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value={"across"}
|
||||
label={t(
|
||||
"promotions.form.allocation.across.title"
|
||||
)}
|
||||
description={t(
|
||||
"promotions.form.allocation.across.description"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export const cartFieldsForRefreshSteps = [
|
||||
"shipping_methods.*",
|
||||
"shipping_methods.adjustments.*",
|
||||
"shipping_methods.tax_lines.*",
|
||||
"shipping_methods.shipping_option.shipping_option_type_id",
|
||||
"customer.*",
|
||||
"customer.groups.*",
|
||||
"promotions.id",
|
||||
|
||||
@@ -399,6 +399,7 @@ export class Promotion {
|
||||
*
|
||||
* @param ruleType - The type of rules to retrieve the attributes for. Can be `rules`, `buy-rules`, or `target-rules`.
|
||||
* @param promotionType - The type of promotion to retrieve the attributes for. It can be `standard` or `buyget`.
|
||||
* @param applicationMethodTargetType - The type of application method to retrieve the attributes for. It can be `order`, `items` or `shipping_methods`.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The list of rule attributes.
|
||||
*
|
||||
@@ -411,6 +412,7 @@ export class Promotion {
|
||||
async listRuleAttributes(
|
||||
ruleType: string,
|
||||
promotionType?: string,
|
||||
applicationMethodTargetType?: string,
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
// eslint-disable-next-line max-len
|
||||
@@ -418,7 +420,10 @@ export class Promotion {
|
||||
`/admin/promotions/rule-attribute-options/${ruleType}`,
|
||||
{
|
||||
headers,
|
||||
query: { promotion_type: promotionType },
|
||||
query: {
|
||||
promotion_type: promotionType,
|
||||
application_method_target_type: applicationMethodTargetType,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BaseFilterable, OperatorMap } from "../../../dal"
|
||||
import {
|
||||
ApplicationMethodTargetTypeValues,
|
||||
ApplicationMethodTypeValues,
|
||||
PromotionTypeValues,
|
||||
} from "../../../promotion"
|
||||
@@ -64,6 +65,7 @@ export interface AdminGetPromotionsParams
|
||||
export interface AdminGetPromotionRuleParams {
|
||||
promotion_type?: PromotionTypeValues
|
||||
application_method_type?: ApplicationMethodTypeValues
|
||||
application_method_target_type?: ApplicationMethodTargetTypeValues
|
||||
}
|
||||
|
||||
export interface AdminGetPromotionRuleTypeParams extends SelectParams {
|
||||
@@ -80,4 +82,5 @@ export interface AdminGetPromotionsRuleValueParams extends FindParams {
|
||||
* Filter by rule value.
|
||||
*/
|
||||
value?: string | string[]
|
||||
application_method_target_type?: ApplicationMethodTargetTypeValues
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export const CartShippingOption: ModuleJoinerConfig = {
|
||||
isLink: true,
|
||||
isReadOnlyLink: true,
|
||||
extends: [
|
||||
{
|
||||
serviceName: Modules.CART,
|
||||
entity: "ShippingMethod",
|
||||
relationship: {
|
||||
serviceName: Modules.FULFILLMENT,
|
||||
primaryKey: "id",
|
||||
foreignKey: "shipping_option_id",
|
||||
alias: "shipping_option",
|
||||
args: {
|
||||
methodSuffix: "ShippingOptions",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export * from "./cart-customer"
|
||||
export * from "./cart-product"
|
||||
export * from "./cart-region"
|
||||
export * from "./cart-sales-channel"
|
||||
export * from "./cart-shipping-option"
|
||||
export * from "./inventory-level-stock-location"
|
||||
export * from "./line-item-adjustment-promotion"
|
||||
export * from "./order-customer"
|
||||
|
||||
+1
-6
@@ -1,10 +1,5 @@
|
||||
import { IPromotionModuleService } from "@medusajs/framework/types"
|
||||
import {
|
||||
ApplicationMethodType,
|
||||
Modules,
|
||||
PromotionStatus,
|
||||
PromotionType,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { ApplicationMethodType, Modules, PromotionStatus, PromotionType, } from "@medusajs/framework/utils"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "@medusajs/test-utils"
|
||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||
import { createDefaultPromotion } from "../../../__fixtures__/promotion"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./buy-get"
|
||||
export * from "./line-items"
|
||||
export * from "./shipping-methods"
|
||||
export * from "./usage"
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
import {
|
||||
ApplicationMethodAllocation,
|
||||
ApplicationMethodTargetType,
|
||||
ApplicationMethodTargetType as TargetType,
|
||||
calculateAdjustmentAmountFromPromotion,
|
||||
ComputedActions,
|
||||
MathBN,
|
||||
MedusaError,
|
||||
ApplicationMethodTargetType as TargetType,
|
||||
calculateAdjustmentAmountFromPromotion,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { areRulesValidForContext } from "../validations"
|
||||
import { computeActionForBudgetExceeded } from "./usage"
|
||||
@@ -43,29 +43,6 @@ export function getComputedActionsForItems(
|
||||
)
|
||||
}
|
||||
|
||||
export function getComputedActionsForShippingMethods(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
shippingMethods: PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS],
|
||||
appliedPromotionsMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
validateContext("shipping_methods", shippingMethods)
|
||||
|
||||
return applyPromotionToItems(promotion, shippingMethods, appliedPromotionsMap)
|
||||
}
|
||||
|
||||
export function getComputedActionsForOrder(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
itemApplicationContext: PromotionTypes.ComputeActionContext,
|
||||
methodIdPromoValueMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
return getComputedActionsForItems(
|
||||
promotion,
|
||||
itemApplicationContext[TargetType.ITEMS],
|
||||
methodIdPromoValueMap,
|
||||
ApplicationMethodAllocation.ACROSS
|
||||
)
|
||||
}
|
||||
|
||||
function applyPromotionToItems(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
items:
|
||||
|
||||
@@ -123,20 +123,13 @@ export function applyPromotionToShippingMethods(
|
||||
}
|
||||
|
||||
const promotionValue = applicationMethod?.value ?? 0
|
||||
const applicableTotal = method.subtotal
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
const applicableTotal = MathBN.sub(method.subtotal, appliedPromoValue)
|
||||
|
||||
const div = MathBN.eq(totalApplicableValue, 0) ? 1 : totalApplicableValue
|
||||
let applicablePromotionValue = MathBN.sub(
|
||||
MathBN.mult(MathBN.div(applicableTotal, div), promotionValue),
|
||||
appliedPromoValue
|
||||
)
|
||||
let applicablePromotionValue = MathBN.mult(MathBN.div(applicableTotal, totalApplicableValue), promotionValue)
|
||||
|
||||
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||
applicablePromotionValue = MathBN.sub(
|
||||
MathBN.mult(MathBN.div(promotionValue, 100), applicableTotal),
|
||||
appliedPromoValue
|
||||
)
|
||||
applicablePromotionValue = MathBN.mult(MathBN.div(promotionValue, 100), applicableTotal)
|
||||
}
|
||||
|
||||
const amount = MathBN.min(applicablePromotionValue, applicableTotal)
|
||||
|
||||
Reference in new issue
Block a user