diff --git a/.changeset/seven-files-begin.md b/.changeset/seven-files-begin.md new file mode 100644 index 0000000000..0111ea78e5 --- /dev/null +++ b/.changeset/seven-files-begin.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(medusa,types): create promotion flows diff --git a/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts b/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts index 97b29e1304..5f64aa0039 100644 --- a/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts +++ b/integration-tests/modules/__tests__/promotion/admin/create-promotion.spec.ts @@ -1,8 +1,8 @@ -import { IPromotionModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" import { PromotionType } from "@medusajs/utils" -import { createAdminUser } from "../../../../helpers/create-admin-user" import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../../helpers/create-admin-user" jest.setTimeout(50000) @@ -65,7 +65,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "each", - value: "100", + value: 100, max_quantity: 100, target_rules: [ { @@ -144,7 +144,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "each", - value: "100", + value: 100, max_quantity: 100, target_rules: [ { @@ -184,7 +184,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "each", - value: "100", + value: 100, max_quantity: 100, buy_rules: [ { @@ -231,7 +231,7 @@ medusaIntegrationTestRunner({ target_type: "items", type: "fixed", allocation: "each", - value: "100", + value: 100, max_quantity: 100, apply_to_quantity: 1, buy_rules_min_quantity: 1, diff --git a/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts b/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts index 2158088f31..99e1f367a2 100644 --- a/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts +++ b/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts @@ -834,8 +834,8 @@ medusaIntegrationTestRunner({ expect(response.data.values.length).toEqual(2) expect(response.data.values).toEqual( expect.arrayContaining([ - { label: "Afghan Afghani", value: "afn" }, - { label: "Albanian Lek", value: "all" }, + { label: "afn", value: "afn" }, + { label: "all", value: "all" }, ]) ) diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 35e739c5b8..0ff378766f 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -824,6 +824,8 @@ "value": "Value", "campaign": "Campaign", "allocation": "Allocation", + "addCondition": "Add condition", + "clearAll": "Clear all", "conditions": { "rules": { "title": "Who can use this code?", @@ -839,6 +841,10 @@ } } }, + "errors": { + "requiredField": "Required field" + }, + "create": {}, "edit": { "title": "Edit Promotion Details", "rules": { @@ -855,6 +861,9 @@ "title": "Add Promotion To Campaign" }, "form": { + "required": "Required", + "and": "AND", + "selectAttribute": "Select Attribute", "campaign": { "existing": { "title": "Existing Campaign", @@ -875,17 +884,31 @@ }, "automatic": { "title": "Automatic", - "description": "Customers will automatically see this during checkout" + "description": "Customers will see this at checkout" + } + }, + "max_quantity": { + "title": "Maximum Quantity", + "description": "Maximum quantity of items this promotion applies to" + }, + "type": { + "standard": { + "title": "Standard", + "description": "A standard promotion" + }, + "buyget": { + "title": "Buy Get", + "description": "Buy X get Y promotion" } }, "allocation": { "each": { "title": "Each", - "description": "Applies value on each product" + "description": "Applies value on each item" }, "across": { "title": "Across", - "description": "Applies value proportionally across products" + "description": "Applies value across items" } }, "code": { diff --git a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx index 81b3820b4b..3143d4472e 100644 --- a/packages/admin-next/dashboard/src/hooks/api/promotions.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/promotions.tsx @@ -29,7 +29,7 @@ import { const PROMOTIONS_QUERY_KEY = "promotions" as const export const promotionsQueryKeys = { ...queryKeysFactory(PROMOTIONS_QUERY_KEY), - listRules: (id: string, ruleType: string) => [ + listRules: (id: string | null, ruleType: string) => [ PROMOTIONS_QUERY_KEY, id, ruleType, @@ -60,7 +60,7 @@ export const usePromotion = ( } export const usePromotionRules = ( - id: string, + id: string | null, ruleType: string, options?: Omit< UseQueryOptions< diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx index c63e856544..138a27e61c 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v2.tsx @@ -172,6 +172,11 @@ export const v2Routes: RouteObject[] = [ path: "", lazy: () => import("../../v2-routes/promotions/promotion-list"), }, + { + path: "create", + lazy: () => + import("../../v2-routes/promotions/promotion-create"), + }, { path: ":id", lazy: () => @@ -196,7 +201,8 @@ export const v2Routes: RouteObject[] = [ }, { path: ":ruleType/edit", - lazy: () => import("../../v2-routes/promotions/edit-rules"), + lazy: () => + import("../../v2-routes/promotions/common/edit-rules"), }, ], }, diff --git a/packages/admin-next/dashboard/src/routes/invite/invite.tsx b/packages/admin-next/dashboard/src/routes/invite/invite.tsx index 7a688c6d46..2da35b372a 100644 --- a/packages/admin-next/dashboard/src/routes/invite/invite.tsx +++ b/packages/admin-next/dashboard/src/routes/invite/invite.tsx @@ -2,11 +2,11 @@ import { zodResolver } from "@hookform/resolvers/zod" import { UserRoles } from "@medusajs/medusa" import { Alert, Button, Heading, Input, Text, Tooltip } from "@medusajs/ui" import { AnimatePresence, motion } from "framer-motion" +import i18n from "i18next" import { useAdminAcceptInvite } from "medusa-react" import { Trans, useTranslation } from "react-i18next" import { Link, useSearchParams } from "react-router-dom" import * as z from "zod" -import i18n from "i18next" import { useState } from "react" import { useForm } from "react-hook-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx new file mode 100644 index 0000000000..7b33dde445 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/edit-rules-form.tsx @@ -0,0 +1,423 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { XMarkMini } from "@medusajs/icons" +import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types" +import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui" +import i18n from "i18next" +import { Fragment, useState } from "react" +import { useFieldArray, useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Combobox } from "../../../../../../components/common/combobox" +import { Form } from "../../../../../../components/common/form" +import { RouteDrawer } from "../../../../../../components/route-modal" +import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions" +import { RuleTypeValues } from "../../edit-rules" +import { getDisguisedRules } from "./utils" + +type EditPromotionFormProps = { + promotion: PromotionDTO + rules: PromotionRuleDTO[] + ruleType: RuleTypeValues + attributes: any[] + operators: any[] + handleSubmit: any + isSubmitting: boolean +} + +const EditRules = zod.object({ + 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(), + field_type: zod.string().optional(), + }) + ), +}) + +const RuleValueFormField = ({ + identifier, + scope, + valuesFields, + valuesRef, + fieldRule, + attributes, + ruleType, +}) => { + const attribute = attributes?.find( + (attr) => attr.value === fieldRule.attribute + ) + const { values: options = [] } = usePromotionRuleValues( + ruleType, + attribute?.id, + { + enabled: !!attribute?.id && !attribute.disguised, + } + ) + + return ( + { + if (fieldRule.field_type === "number") { + return ( + + + + + + + ) + } else if (fieldRule.field_type === "text") { + return ( + + + + + + + ) + } else { + return ( + + + + + + + + ) + } + }} + /> + ) +} + +export const RulesFormField = ({ + form, + ruleType, + fields, + attributes, + operators, + removeRule, + updateRule, + appendRule, + setRulesToRemove, + rulesToRemove, + scope = "rules", +}) => { + const { t } = useTranslation() + + return ( +
+ + {t(`promotions.fields.conditions.${ruleType}.title`)} + + + + {t(`promotions.fields.conditions.${ruleType}.description`)} + + + {fields.map((fieldRule, index) => { + const identifier = fieldRule.id + const { ref: attributeRef, ...attributeFields } = form.register( + `${scope}.${index}.attribute` + ) + const { ref: operatorRef, ...operatorFields } = form.register( + `${scope}.${index}.operator` + ) + const { ref: valuesRef, ...valuesFields } = form.register( + `${scope}.${index}.values` + ) + + return ( + +
+
+ { + const existingAttributes = + fields?.map((field) => field.attribute) || [] + const attributeOptions = + attributes?.filter((attr) => { + if (attr.value === fieldRule.attribute) { + return true + } + + return !existingAttributes.includes(attr.value) + }) || [] + + return ( + + {fieldRule.required && ( +

+ {t("promotions.form.required")} +

+ )} + + + + + +
+ ) + }} + /> + +
+ { + return ( + + + + + + + ) + }} + /> + + +
+
+ +
+ { + if (!fieldRule.required) { + fieldRule.id && + setRulesToRemove && + setRulesToRemove([...rulesToRemove, fieldRule]) + + removeRule(index) + } + }} + /> +
+
+ + {index < fields.length - 1 && ( +
+
+ + + {t("promotions.form.and")} + +
+ )} +
+ ) + })} + +
+ + + +
+
+ ) +} + +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>({ + defaultValues: { + rules: [...disguisedRules, ...rules].map((rule) => ({ + id: rule.id, + required: requiredAttributeValues.includes(rule.attribute), + field_type: rule.field_type, + attribute: rule.attribute!, + operator: rule.operator!, + values: rule?.values?.map((v: { value: string }) => v.value!), + })), + }, + resolver: zodResolver(EditRules), + }) + + const { fields, append, remove, update } = useFieldArray({ + control: form.control, + name: "rules", + keyName: "rules_id", + }) + + const handleFormSubmit = form.handleSubmit(handleSubmit(rulesToRemove)) + + return ( + +
+ + + + + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/index.ts rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/index.ts diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts similarity index 100% rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/utils.ts rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-form/utils.ts diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx new file mode 100644 index 0000000000..1458990fc4 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/edit-rules-wrapper.tsx @@ -0,0 +1,137 @@ +import { PromotionDTO, PromotionRuleDTO } 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" + +type EditPromotionFormProps = { + promotion: PromotionDTO + rules: PromotionRuleDTO[] + ruleType: RuleTypeValues +} + +export const EditRulesWrapper = ({ + promotion, + rules, + 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, + ruleType + ) + + const { mutateAsync: removePromotionRules } = usePromotionRemoveRules( + promotion.id, + ruleType + ) + + const { mutateAsync: updatePromotionRules, isPending } = + usePromotionUpdateRules(promotion.id, ruleType) + + const handleSubmit = (rulesToRemove?: any[]) => { + return async function (data) { + const applicationMethodData: Record = {} + const { rules: allRules = [] } = data + + const disguisedRulesData = allRules.filter((rule) => + disguisedRules.map((rule) => rule.id).includes(rule.id!) + ) + + // 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) { + applicationMethodData[rule.id!] = parseInt(rule.values as string) + } + + // 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 rulesToCreate = rulesData.filter((rule) => !("id" in rule)) + const rulesToUpdate = rulesData.filter( + (rule) => typeof rule.id === "string" + ) + + if (Object.keys(applicationMethodData).length) { + await updatePromotion({ application_method: applicationMethodData }) + } + + rulesToCreate.length && + (await addPromotionRules({ + rules: rulesToCreate.map((rule) => { + return { + attribute: rule.attribute, + operator: rule.operator, + values: rule.values, + } as any + }), + })) + + rulesToRemove?.length && + (await removePromotionRules({ + rule_ids: rulesToRemove.map((r) => r.id!), + })) + + rulesToUpdate.length && + (await updatePromotionRules({ + rules: rulesToUpdate.map((rule) => { + return { + id: rule.id!, + attribute: rule.attribute, + operator: rule.operator, + values: rule.values, + } as any + }), + })) + + handleSuccess() + } + } + + if (attributes && operators) { + return ( + + ) + } +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/index.ts new file mode 100644 index 0000000000..f8a67aac9f --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/components/edit-rules-wrapper/index.ts @@ -0,0 +1 @@ +export * from "./edit-rules-wrapper" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/edit-rules.tsx similarity index 63% rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/edit-rules.tsx index 969dabb1ba..437d571592 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/edit-rules.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/edit-rules.tsx @@ -2,13 +2,9 @@ import { PromotionRuleDTO } from "@medusajs/types" import { Heading } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" -import { RouteDrawer } from "../../../components/route-modal" -import { - usePromotion, - usePromotionRuleAttributes, - usePromotionRuleOperators, -} from "../../../hooks/api/promotions" -import { EditRulesForm } from "./components/edit-rules-form" +import { RouteDrawer } from "../../../../components/route-modal" +import { usePromotion } from "../../../../hooks/api/promotions" +import { EditRulesWrapper } from "./components/edit-rules-wrapper" export enum RuleType { RULES = "rules", @@ -33,17 +29,7 @@ export const EditRules = () => { const ruleType = params.ruleType as RuleTypeValues const id = params.id as string const rules: PromotionRuleDTO[] = [] - const { promotion, isLoading, isError, error } = usePromotion(id) - const { - attributes, - isError: isAttributesError, - error: attributesError, - } = usePromotionRuleAttributes(ruleType!) - const { - operators, - isError: isOperatorsError, - error: operatorsError, - } = usePromotionRuleOperators() + const { promotion, isPending: isLoading, isError, error } = usePromotion(id) if (promotion) { if (ruleType === RuleType.RULES) { @@ -55,8 +41,8 @@ export const EditRules = () => { } } - if (isError || isAttributesError || isOperatorsError) { - throw error || attributesError || operatorsError + if (isError) { + throw error } return ( @@ -65,13 +51,11 @@ export const EditRules = () => { {t(`promotions.edit.${ruleType}.title`)} - {!isLoading && promotion && attributes && operators && ( - )} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/index.ts rename to packages/admin-next/dashboard/src/v2-routes/promotions/common/edit-rules/index.ts diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx deleted file mode 100644 index c93a3eb04f..0000000000 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/edit-rules/components/edit-rules-form/edit-rules-form.tsx +++ /dev/null @@ -1,442 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { XMarkMini } from "@medusajs/icons" -import { PromotionDTO, PromotionRuleDTO } from "@medusajs/types" -import { Badge, Button, Heading, Input, Select, Text } from "@medusajs/ui" -import { Fragment, useState } from "react" -import { useFieldArray, useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import * as zod from "zod" -import { Combobox } from "../../../../../components/common/combobox" -import { Form } from "../../../../../components/common/form" -import { - RouteDrawer, - useRouteModal, -} from "../../../../../components/route-modal" -import { - usePromotionAddRules, - usePromotionRemoveRules, - usePromotionRuleValues, - usePromotionUpdateRules, - useUpdatePromotion, -} from "../../../../../hooks/api/promotions" -import { RuleTypeValues } from "../../edit-rules" -import { getDisguisedRules } from "./utils" - -type EditPromotionFormProps = { - promotion: PromotionDTO - rules: PromotionRuleDTO[] - ruleType: RuleTypeValues - attributes: any[] - operators: any[] -} - -const EditRules = zod.object({ - rules: zod.array( - zod.object({ - id: zod.string().optional(), - attribute: zod.string().min(1, { message: "Required field" }), - operator: zod.string().min(1, { message: "Required field" }), - values: zod.union([ - zod.number().min(1, { message: "Required field" }), - zod.string().min(1, { message: "Required field" }), - zod.array(zod.string()).min(1, { message: "Required field" }), - ]), - required: zod.boolean().optional(), - field_type: zod.string().optional(), - }) - ), -}) - -const fetchOptionsForRule = (ruleType: string, attribute: string) => { - const { values } = usePromotionRuleValues(ruleType, attribute) - - return values || [] -} - -export const EditRulesForm = ({ - promotion, - rules, - ruleType, - attributes, - operators, -}: EditPromotionFormProps) => { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - 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>({ - defaultValues: { - rules: [...disguisedRules, ...rules].map((rule) => ({ - id: rule.id, - required: requiredAttributeValues.includes(rule.attribute), - field_type: rule.field_type, - attribute: rule.attribute!, - operator: rule.operator!, - values: rule?.values?.map((v: { value: string }) => v.value!), - })), - }, - resolver: zodResolver(EditRules), - }) - - const { fields, append, remove, update } = useFieldArray({ - control: form.control, - name: "rules", - keyName: "rules_id", - }) - - const { mutateAsync: updatePromotion } = useUpdatePromotion(promotion.id) - const { mutateAsync: addPromotionRules } = usePromotionAddRules( - promotion.id, - ruleType - ) - - const { mutateAsync: removePromotionRules } = usePromotionRemoveRules( - promotion.id, - ruleType - ) - - const { mutateAsync: updatePromotionRules } = usePromotionUpdateRules( - promotion.id, - ruleType - ) - - const handleSubmit = form.handleSubmit(async (data) => { - const applicationMethodData: Record = {} - const { rules: allRules = [] } = data - - const disguisedRulesData = allRules.filter((rule) => - disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!) - ) - - // 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) { - applicationMethodData[rule.id!] = parseInt(rule.values as string) - } - - // This variable will contain the rules that are actual rule objects, without the disguised - // objects - const rulesData = allRules.filter( - (rule) => - !disguisedRules.map((maskedRule) => maskedRule.id).includes(rule.id!) - ) - - const rulesToCreate = rulesData.filter((rule) => !("id" in rule)) - const rulesToUpdate = rulesData.filter( - (rule) => typeof rule.id === "string" - ) - - if (Object.keys(applicationMethodData).length) { - await updatePromotion({ application_method: applicationMethodData }) - } - - rulesToCreate.length && - (await addPromotionRules({ - rules: rulesToCreate.map((rule) => { - return { - attribute: rule.attribute, - operator: rule.operator, - values: rule.values, - } as any - }), - })) - - rulesToRemove.length && - (await removePromotionRules({ - rule_ids: rulesToRemove.map((r) => r.id!), - })) - - rulesToUpdate.length && - (await updatePromotionRules({ - rules: rulesToUpdate.map((rule) => { - return { - id: rule.id!, - attribute: rule.attribute, - operator: rule.operator, - values: rule.values, - } as any - }), - })) - - handleSuccess() - }) - - return ( - -
- -
- - {t(`promotions.fields.conditions.${ruleType}.title`)} - - - - {t(`promotions.fields.conditions.${ruleType}.description`)} - - - {fields.map((fieldRule, index) => { - const identifier = fieldRule.id - const { ref: attributeRef, ...attributeFields } = form.register( - `rules.${index}.attribute` - ) - const { ref: operatorRef, ...operatorFields } = form.register( - `rules.${index}.operator` - ) - const { ref: valuesRef, ...valuesFields } = form.register( - `rules.${index}.values` - ) - - return ( - -
-
- { - const existingAttributes = - fields?.map((field) => field.attribute) || [] - const attributeOptions = - attributes?.filter((attr) => { - if (attr.value === fieldRule.attribute) { - return true - } - - return !existingAttributes.includes(attr.value) - }) || [] - - return ( - - {fieldRule.required && ( -

- Required -

- )} - - - - - -
- ) - }} - /> - -
- { - return ( - - - - - - - ) - }} - /> - - { - if (fieldRule.field_type === "number") { - return ( - - - - - - - ) - } else if (fieldRule.field_type === "text") { - return ( - - - - - - - ) - } else { - const attribute = attributes?.find( - (attr) => attr.value === fieldRule.attribute - ) - const options = attribute - ? fetchOptionsForRule(ruleType, attribute.id) - : [] - - return ( - - - - - - - - ) - } - }} - /> -
-
- -
- { - if (!fieldRule.required) { - if (fieldRule.id) { - setRulesToRemove([...rulesToRemove, fieldRule]) - } - - remove(index) - } - }} - /> -
-
- - {index < fields.length - 1 && ( -
-
- - - AND - -
- )} -
- ) - })} - -
- - - -
-
-
- - -
- - - - - -
-
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx index a17bc289f0..adaeb1acad 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-add-campaign/components/add-campaign-promotion-form/add-campaign-promotion-form.tsx @@ -1,6 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { CampaignDTO, PromotionDTO } from "@medusajs/types" -import { Button, RadioGroup, Select } from "@medusajs/ui" +import { Button, clx, RadioGroup, Select } from "@medusajs/ui" import { useForm, useWatch } from "react-hook-form" import { useTranslation } from "react-i18next" import * as zod from "zod" @@ -23,6 +23,97 @@ const EditPromotionSchema = zod.object({ existing: zod.string().toLowerCase(), }) +export const AddCampaignPromotionFields = ({ form, campaigns }) => { + const { t } = useTranslation() + const watchCampaignId = useWatch({ + control: form.control, + name: "campaign_id", + }) + + const selectedCampaign = campaigns.find((c) => c.id === watchCampaignId) + + return ( +
+ { + return ( + + Method + + + + + + + + + + ) + }} + /> + + { + return ( + + + {t("promotions.form.campaign.existing.title")} + + + + + + + + ) + }} + /> + + +
+ ) +} + export const AddCampaignPromotionForm = ({ promotion, campaigns, @@ -58,78 +149,7 @@ export const AddCampaignPromotionForm = ({
-
- { - return ( - - Method - - - - - - - - - - ) - }} - /> - - { - return ( - - - {t("promotions.form.campaign.existing.title")} - - - - - - - - ) - }} - /> - - -
+
diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/constants.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/constants.ts new file mode 100644 index 0000000000..9ba8032be0 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/constants.ts @@ -0,0 +1,11 @@ +export enum View { + TEMPLATE = "template", + PROMOTION = "promotion", + CAMPAIGN = "campaign", +} + +export enum Tab { + TYPE = "type", + PROMOTION = "promotion", + CAMPAIGN = "campaign", +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx new file mode 100644 index 0000000000..d10ecd3b46 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/create-promotion-form.tsx @@ -0,0 +1,775 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { + Alert, + Button, + clx, + CurrencyInput, + Input, + ProgressTabs, + RadioGroup, + Text, +} from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" +import { useFieldArray, useForm, useWatch } from "react-hook-form" +import { Trans, useTranslation } from "react-i18next" +import { z } from "zod" + +import { + CampaignResponse, + PromotionRuleResponse, + RuleAttributeOptionsResponse, + RuleOperatorOptionsResponse, +} from "@medusajs/types" +import { Form } from "../../../../../components/common/form" +import { PercentageInput } from "../../../../../components/common/percentage-input" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/route-modal" +import { useCreatePromotion } from "../../../../../hooks/api/promotions" +import { getCurrencySymbol } from "../../../../../lib/currencies" +import { RulesFormField } from "../../../common/edit-rules/components/edit-rules-form" +import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form" +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[] + campaigns: CampaignResponse[] +} + +export const CreatePromotionForm = ({ + ruleAttributes, + targetRuleAttributes, + buyRuleAttributes, + operators, + rules, + targetRules, + buyRules, + campaigns, +}: CreatePromotionFormProps) => { + const [tab, setTab] = useState(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>({ + defaultValues: { + campaign_id: undefined, + template_id: templates[0].id!, + existing: "true", + is_automatic: "false", + code: "", + type: "standard", + rules: generateRuleAttributes(rules), + application_method: { + allocation: "each", + type: "fixed", + target_type: "items", + max_quantity: 1, + target_rules: generateRuleAttributes(targetRules), + buy_rules: generateRuleAttributes(buyRules), + }, + }, + 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( + async (data) => { + const { + existing, + is_automatic, + template_id, + application_method, + rules, + ...promotionData + } = data + const { + target_rules: targetRulesData = [], + buy_rules: buyRulesData = [], + ...applicationMethodData + } = application_method + + const disguisedRuleAttributes = [ + ...targetRules.filter((r) => !!r.disguised), + ...buyRules.filter((r) => !!r.disguised), + ].map((r) => r.attribute) + + const attr: Record = {} + + for (const rule of [...targetRulesData, ...buyRulesData]) { + if (disguisedRuleAttributes.includes(rule.attribute)) { + attr[rule.attribute] = rule.values + } + } + + createPromotion({ + ...promotionData, + rules: rules.map((rule) => ({ + operator: rule.operator, + attribute: rule.attribute, + values: rule.values, + })), + application_method: { + ...applicationMethodData, + ...attr, + target_rules: targetRulesData + .filter((r) => !disguisedRuleAttributes.includes(r.attribute)) + .map((rule) => ({ + operator: rule.operator, + attribute: rule.attribute, + values: rule.values, + })), + buy_rules: buyRulesData + .filter((r) => !disguisedRuleAttributes.includes(r.attribute)) + .map((rule) => ({ + operator: rule.operator, + attribute: rule.attribute, + values: rule.values, + })), + }, + is_automatic: is_automatic === "true", + }).then(() => handleSuccess()) + }, + async (error) => { + // TODO: showcase error when something goes wrong + // Wait for alert component and use it here + } + ) + + const handleContinue = async () => { + switch (tab) { + case Tab.TYPE: + setTab(Tab.PROMOTION) + break + case Tab.PROMOTION: + const valid = await form.trigger() + + if (valid) { + setTab(Tab.CAMPAIGN) + } else { + // TODO: Set errors on the root level + } + + break + case Tab.CAMPAIGN: + break + } + } + + const handleTabChange = (tab: Tab) => { + switch (tab) { + case Tab.TYPE: + setDetailsValidated(false) + setTab(tab) + break + case Tab.PROMOTION: + setDetailsValidated(false) + setTab(tab) + break + case Tab.CAMPAIGN: + setDetailsValidated(false) + setTab(tab) + break + } + } + + const watchTemplateId = useWatch({ + control: form.control, + name: "template_id", + }) + + useMemo(() => { + const currentTemplate = templates.find( + (template) => template.id === watchTemplateId + ) + + if (!currentTemplate) { + return + } + + for (const [key, value] of Object.entries(currentTemplate.defaults)) { + if (typeof value === "object") { + for (const [subKey, subValue] of Object.entries(value)) { + form.setValue(`application_method.${subKey}`, subValue) + } + } else { + form.setValue(key, value) + } + } + }, [watchTemplateId]) + + const watchValueType = useWatch({ + control: form.control, + name: "application_method.type", + }) + + const isFixedValueType = watchValueType === "fixed" + + const watchAllocation = useWatch({ + control: form.control, + name: "application_method.allocation", + }) + + const isAllocationEach = watchAllocation === "each" + + const watchType = useWatch({ + control: form.control, + name: "type", + }) + + const isTypeStandard = watchType === "standard" + + 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" + } + + return "not-started" + }, [detailsValidated]) + + return ( + + + handleTabChange(tab as Tab)} + > + +
+
+ + + {t("fields.type")} + + + + {t("fields.details")} + + + + {t("promotions.fields.campaign")} + + +
+ +
+ + + + + {tab === Tab.CAMPAIGN ? ( + + ) : ( + + )} +
+
+
+ + + + { + return ( + + {t("promotions.fields.type")} + + + {templates.map((template) => { + return ( + + ) + })} + + + + + ) + }} + /> + + + + {form.formState.errors.root && ( + + {form.formState.errors.root.message} + + )} + + { + return ( + + Method + + + + + + + + + ) + }} + /> + +
+ { + return ( + + + {t("promotions.form.code.title")} + + + + + + + + ]} + /> + + + ) + }} + /> +
+ + { + return ( + + + {t("promotions.fields.value_type")} + + + + + + + + + + + ) + }} + /> + +
+ { + return ( + + + {isFixedValueType + ? t("fields.amount") + : t("fields.percentage")} + + + {isFixedValueType ? ( + { + onChange(value ? parseInt(value) : "") + }} + code={"USD"} + symbol={getCurrencySymbol("USD")} + {...field} + value={value} + /> + ) : ( + { + onChange( + e.target.value === "" + ? null + : parseInt(e.target.value) + ) + }} + /> + )} + + + + ) + }} + /> +
+ + { + return ( + + {t("promotions.fields.type")} + + + + + + + + + + ) + }} + /> + + {isTypeStandard && ( + { + return ( + + + {t("promotions.fields.allocation")} + + + + + + + + + + + + ) + }} + /> + )} + + {isTypeStandard && isAllocationEach && ( +
+ { + return ( + + + {t("promotions.form.max_quantity.title")} + + + + + + + + ]} + /> + + + ) + }} + /> +
+ )} + + + + + + {!isTypeStandard && ( + + )} +
+ + + + +
+
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/form-schema.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/form-schema.ts new file mode 100644 index 0000000000..698630c4c5 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/form-schema.ts @@ -0,0 +1,36 @@ +import { z } from "zod" + +const RuleSchema = z.array( + z.object({ + id: z.string().optional(), + attribute: z.string().min(1, { message: "Required field" }), + operator: z.string().min(1, { message: "Required field" }), + values: z.union([ + z.number().min(1, { message: "Required field" }), + z.string().min(1, { message: "Required field" }), + z.array(z.string()).min(1, { message: "Required field" }), + ]), + required: z.boolean().optional(), + disguised: z.boolean().optional(), + field_type: z.string().optional(), + }) +) + +export const CreatePromotionSchema = z.object({ + template_id: z.string().optional(), + campaign_id: z.string().optional(), + existing: z.string().toLowerCase().optional(), + is_automatic: z.string().toLowerCase(), + code: z.string().min(1), + type: z.enum(["buyget", "standard"]), + rules: RuleSchema, + application_method: z.object({ + allocation: z.enum(["each", "across"]), + value: z.number().min(0), + max_quantity: z.number().optional(), + target_rules: RuleSchema, + buy_rules: RuleSchema.min(2).optional(), + type: z.enum(["fixed", "percentage"]), + target_type: z.enum(["order", "shipping_methods", "items"]), + }), +}) diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/index.ts new file mode 100644 index 0000000000..be0d550e01 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/index.ts @@ -0,0 +1 @@ +export * from "./create-promotion-form" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/templates.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/templates.ts new file mode 100644 index 0000000000..dcebe9e99b --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/components/create-promotion-form/templates.ts @@ -0,0 +1,62 @@ +export const templates = [ + { + id: "amount_off_products", + type: "standard", + title: "Amount off products", + description: "Discount specific products or collection of products", + defaults: { + is_automatic: "false", + type: "standard", + application_method: { + allocation: "each", + target_type: "items", + type: "fixed", + }, + }, + }, + { + id: "amount_off_order", + type: "standard", + title: "Amount off order", + description: "Discounts the total order amount", + defaults: { + is_automatic: "false", + type: "standard", + application_method: { + allocation: "across", + target_type: "order", + type: "fixed", + }, + }, + }, + { + id: "percentage_off_product", + type: "standard", + title: "Percentage off product", + description: "Discounts a percentage off selected products", + defaults: { + is_automatic: "false", + type: "standard", + application_method: { + allocation: "each", + target_type: "items", + type: "percentage", + }, + }, + }, + { + id: "percentage_off_order", + type: "standard", + title: "Percentage off order", + description: "Discounts a percentage of the total order amount", + defaults: { + is_automatic: "false", + type: "standard", + application_method: { + allocation: "across", + target_type: "items", + type: "percentage", + }, + }, + }, +] diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/index.ts b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/index.ts new file mode 100644 index 0000000000..54516408a8 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/index.ts @@ -0,0 +1 @@ +export { PromotionCreate as Component } from "./promotion-create" diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/promotion-create.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/promotion-create.tsx new file mode 100644 index 0000000000..ee96dffc90 --- /dev/null +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-create/promotion-create.tsx @@ -0,0 +1,46 @@ +import { RouteFocusModal } from "../../../components/route-modal" +import { useCampaigns } from "../../../hooks/api/campaigns" +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() + const { campaigns } = useCampaigns() + + return ( + + {rules && + buyRules && + targetRules && + campaigns && + operators && + ruleAttributes && + targetRuleAttributes && + buyRuleAttributes && ( + + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx index 7fbe9ec95c..a9e7ea4675 100644 --- a/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx +++ b/packages/admin-next/dashboard/src/v2-routes/promotions/promotion-edit-details/components/edit-promotion-form/edit-promotion-details-form.tsx @@ -1,6 +1,13 @@ import { zodResolver } from "@hookform/resolvers/zod" import { PromotionDTO } from "@medusajs/types" -import { Button, CurrencyInput, Input, RadioGroup, Text } from "@medusajs/ui" +import { + Button, + clx, + CurrencyInput, + Input, + RadioGroup, + Text, +} from "@medusajs/ui" import { useForm, useWatch } from "react-hook-form" import { Trans, useTranslation } from "react-i18next" import * as zod from "zod" @@ -53,7 +60,6 @@ export const EditPromotionDetailsForm = ({ const { mutateAsync, isPending } = useUpdatePromotion(promotion.id) const handleSubmit = form.handleSubmit(async (data) => { - console.log("data ------ ", data) await mutateAsync( { is_automatic: data.is_automatic === "true", @@ -92,6 +98,10 @@ export const EditPromotionDetailsForm = ({ onValueChange={field.onChange} > !!attr.disguised) + const requiredRules = ruleAttributes.filter((attr) => !!attr.required) for (const disguisedRule of disguisedRules) { - const value = promotion.application_method?.[disguisedRule.id] - const values = [{ label: value, value }] + const value = promotion?.application_method?.[disguisedRule.id] + const values = value ? [{ label: value, value }] : [] transformedRules.push({ id: undefined, attribute: disguisedRule.id, attribute_label: disguisedRule.label, + field_type: disguisedRule.field_type, operator: RuleOperator.EQ, operator_label: operatorsMap[RuleOperator.EQ].label, values, @@ -102,6 +104,7 @@ export const GET = async ( transformedRules.push({ ...promotionRule, attribute_label: currentRuleAttribute.label, + field_type: currentRuleAttribute.field_type, operator_label: operatorsMap[promotionRule.operator]?.label || promotionRule.operator, disguised: false, @@ -109,6 +112,24 @@ export const GET = async ( }) } + 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, + }) + + continue + } + } + res.json({ rules: transformedRules, }) diff --git a/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts b/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts index 61e9a1de9d..78ec9687e9 100644 --- a/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts +++ b/packages/medusa/src/api-v2/admin/promotions/utils/rule-query-configuration.ts @@ -6,7 +6,7 @@ export const ruleQueryConfigurations = { }, currency: { entryPoint: "currency", - labelAttr: "name", + labelAttr: "code", valueAttr: "code", }, customer_group: { diff --git a/packages/medusa/src/api-v2/admin/promotions/validators.ts b/packages/medusa/src/api-v2/admin/promotions/validators.ts index 00bb56cb84..17adcbc97a 100644 --- a/packages/medusa/src/api-v2/admin/promotions/validators.ts +++ b/packages/medusa/src/api-v2/admin/promotions/validators.ts @@ -1,9 +1,3 @@ -import { z } from "zod" -import { - createFindParams, - createOperatorMap, - createSelectParams, -} from "../../utils/validators" import { ApplicationMethodAllocation, ApplicationMethodTargetType, @@ -12,6 +6,12 @@ import { PromotionRuleOperator, PromotionType, } from "@medusajs/utils" +import { z } from "zod" +import { + createFindParams, + createOperatorMap, + createSelectParams, +} from "../../utils/validators" export type AdminGetPromotionParamsType = z.infer< typeof AdminGetPromotionParams @@ -89,7 +89,7 @@ export type AdminCreateApplicationMethodType = z.infer< export const AdminCreateApplicationMethod = z .object({ description: z.string().optional(), - value: z.string(), + value: z.number(), max_quantity: z.number().optional(), type: z.nativeEnum(ApplicationMethodType), target_type: z.nativeEnum(ApplicationMethodTargetType), diff --git a/packages/types/src/http/campaign/admin/campaign.ts b/packages/types/src/http/campaign/admin/campaign.ts new file mode 100644 index 0000000000..5ebece8ec0 --- /dev/null +++ b/packages/types/src/http/campaign/admin/campaign.ts @@ -0,0 +1,34 @@ +import { PaginatedResponse } from "../../common" + +/** + * @experimental + */ +export interface CampaignResponse { + id: string + name: string + description: string + currency: string + campaign_identifier: string + starts_at: string + ends_at: string + budget: { + id: string + type: string + limit: number | null + used: number + } +} + +/** + * @experimental + */ +export interface AdminCampaignListResponse extends PaginatedResponse { + campaigns: CampaignResponse[] +} + +/** + * @experimental + */ +export interface AdminCampaignResponse { + campaign: CampaignResponse +} diff --git a/packages/types/src/http/campaign/admin/index.ts b/packages/types/src/http/campaign/admin/index.ts new file mode 100644 index 0000000000..8d91a02bcd --- /dev/null +++ b/packages/types/src/http/campaign/admin/index.ts @@ -0,0 +1 @@ +export * from "./campaign" diff --git a/packages/types/src/http/campaign/index.ts b/packages/types/src/http/campaign/index.ts new file mode 100644 index 0000000000..26b8eb9dad --- /dev/null +++ b/packages/types/src/http/campaign/index.ts @@ -0,0 +1 @@ +export * from "./admin" diff --git a/packages/types/src/http/index.ts b/packages/types/src/http/index.ts index b839f776b3..37a616dbb7 100644 --- a/packages/types/src/http/index.ts +++ b/packages/types/src/http/index.ts @@ -1,10 +1,12 @@ export * from "./api-key" +export * from "./campaign" export * from "./customer" export * from "./fulfillment" export * from "./inventory" export * from "./order" export * from "./pricing" export * from "./product-category" +export * from "./promotion" export * from "./sales-channel" export * from "./stock-locations" export * from "./tax" diff --git a/packages/types/src/http/promotion/admin/index.ts b/packages/types/src/http/promotion/admin/index.ts new file mode 100644 index 0000000000..86cbb5a6d5 --- /dev/null +++ b/packages/types/src/http/promotion/admin/index.ts @@ -0,0 +1,3 @@ +export * from "./promotion-rule" +export * from "./rule-attribute-options" +export * from "./rule-operator-options" diff --git a/packages/types/src/http/promotion/admin/promotion-rule.ts b/packages/types/src/http/promotion/admin/promotion-rule.ts new file mode 100644 index 0000000000..e813f2d5c8 --- /dev/null +++ b/packages/types/src/http/promotion/admin/promotion-rule.ts @@ -0,0 +1,21 @@ +/** + * @experimental + */ +export interface PromotionRuleResponse { + id: string + attribute: string + attribute_label: string + field_type: string + operator: string + operator_label: string + values: { value: string }[] + disguised: boolean + required: boolean +} + +/** + * @experimental + */ +export interface AdminPromotionRuleListResponse { + attributes: PromotionRuleResponse[] +} diff --git a/packages/types/src/http/promotion/admin/rule-attribute-options.ts b/packages/types/src/http/promotion/admin/rule-attribute-options.ts new file mode 100644 index 0000000000..a2f7fd0098 --- /dev/null +++ b/packages/types/src/http/promotion/admin/rule-attribute-options.ts @@ -0,0 +1,18 @@ +/** + * @experimental + */ +export interface RuleAttributeOptionsResponse { + id: string + value: string + label: string + field_type: string + required: boolean + disguised: boolean +} + +/** + * @experimental + */ +export interface AdminRuleAttributeOptionsListResponse { + attributes: RuleAttributeOptionsResponse[] +} diff --git a/packages/types/src/http/promotion/admin/rule-operator-options.ts b/packages/types/src/http/promotion/admin/rule-operator-options.ts new file mode 100644 index 0000000000..11f14d1b18 --- /dev/null +++ b/packages/types/src/http/promotion/admin/rule-operator-options.ts @@ -0,0 +1,15 @@ +/** + * @experimental + */ +export interface RuleOperatorOptionsResponse { + id: string + value: string + label: string +} + +/** + * @experimental + */ +export interface AdminRuleOperatorOptionsListResponse { + operators: RuleOperatorOptionsResponse[] +} diff --git a/packages/types/src/http/promotion/index.ts b/packages/types/src/http/promotion/index.ts new file mode 100644 index 0000000000..26b8eb9dad --- /dev/null +++ b/packages/types/src/http/promotion/index.ts @@ -0,0 +1 @@ +export * from "./admin" diff --git a/packages/types/src/promotion/http.ts b/packages/types/src/promotion/http.ts index 8942ade390..9dc5652ce9 100644 --- a/packages/types/src/promotion/http.ts +++ b/packages/types/src/promotion/http.ts @@ -2,9 +2,10 @@ export type AdminGetPromotionRulesRes = { id?: string attribute: string attribute_label: string + field_type?: string operator: string operator_label: string - values: { label?: string; value: string }[] + values: { label?: string; value?: string }[] disguised?: boolean required?: boolean }[]