diff --git a/packages/modules/b2c-core/src/api/vendor/promotions/middlewares.ts b/packages/modules/b2c-core/src/api/vendor/promotions/middlewares.ts index ee9d54fa6..f0f52ef12 100644 --- a/packages/modules/b2c-core/src/api/vendor/promotions/middlewares.ts +++ b/packages/modules/b2c-core/src/api/vendor/promotions/middlewares.ts @@ -25,6 +25,7 @@ import { VendorUpdatePromotion, } from "./validators"; import { vendorPromotionsRuleValueOptionsPathParamsGuard } from "../../../shared/infra/http/middlewares/vendor-promotions-rule-value-options-path-params-guard"; +import { vendorPromotionsRuleAttributeOptionsPathParamsGuard } from "../../../shared/infra/http/middlewares/vendor-promotions-rule-attribute-options-path-params-guard"; export const vendorPromotionsMiddlewares: MiddlewareRoute[] = [ { @@ -166,6 +167,7 @@ export const vendorPromotionsMiddlewares: MiddlewareRoute[] = [ method: ["GET"], matcher: "/vendor/promotions/rule-attribute-options/:rule_type", middlewares: [ + vendorPromotionsRuleAttributeOptionsPathParamsGuard, validateAndTransformQuery( VendorGetPromotionRuleParams, vendorRuleTransformQueryConfig.list diff --git a/packages/modules/b2c-core/src/api/vendor/promotions/rule-attribute-options/[rule_type]/route.ts b/packages/modules/b2c-core/src/api/vendor/promotions/rule-attribute-options/[rule_type]/route.ts index 32d06b441..7cdbee044 100644 --- a/packages/modules/b2c-core/src/api/vendor/promotions/rule-attribute-options/[rule_type]/route.ts +++ b/packages/modules/b2c-core/src/api/vendor/promotions/rule-attribute-options/[rule_type]/route.ts @@ -1,7 +1,10 @@ -import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' -import { validateRuleType } from '@medusajs/medusa/api/admin/promotions/utils/validate-rule-type' +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework"; -import { getRuleAttributesMap } from '../../utils' +import "../../../../../shared/infra/http/middlewares/types"; // Import type augmentation +import { getRuleAttributesMap } from "../../utils"; /** * @oas [get] /vendor/promotions/rule-attribute-options/{rule_type} @@ -69,17 +72,16 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const { rule_type: ruleType } = req.params + const ruleType = req.normalized_rule_type || req.params.rule_type; - validateRuleType(ruleType) + const ruleAttributesMap = getRuleAttributesMap({ + promotionType: req.query.promotion_type as string, + applicationMethodType: req.query.application_method_type as string, + }); - const attributes = - getRuleAttributesMap({ - promotionType: req.query.promotion_type as string, - applicationMethodType: req.query.application_method_type as string - })[ruleType] || [] + const attributes = ruleAttributesMap[ruleType] || []; res.json({ - attributes - }) -} + attributes, + }); +}; diff --git a/packages/modules/b2c-core/src/api/vendor/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts b/packages/modules/b2c-core/src/api/vendor/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts index 7aa95aeb6..06625f087 100644 --- a/packages/modules/b2c-core/src/api/vendor/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts +++ b/packages/modules/b2c-core/src/api/vendor/promotions/rule-value-options/[rule_type]/[rule_attribute_id]/route.ts @@ -1,20 +1,18 @@ -import { AuthenticatedMedusaRequest, MedusaResponse } from '@medusajs/framework' import { - ApplicationMethodTypeValues, - PromotionTypeValues, - RuleTypeValues -} from '@medusajs/framework/types' + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework"; import { ContainerRegistrationKeys, - remoteQueryObjectFromString -} from '@medusajs/framework/utils' -import { ruleQueryConfigurations } from '@medusajs/medusa/api/admin/promotions/utils/rule-query-configuration' -import { validateRuleAttribute } from '@medusajs/medusa/api/admin/promotions/utils/validate-rule-attribute' -import { validateRuleType } from '@medusajs/medusa/api/admin/promotions/utils/validate-rule-type' + remoteQueryObjectFromString, +} from "@medusajs/framework/utils"; +import { ruleQueryConfigurations } from "@medusajs/medusa/api/admin/promotions/utils/rule-query-configuration"; -import sellerCustomerGroup from '../../../../../../links/seller-customer-group' -import sellerProduct from '../../../../../../links/seller-product' -import { fetchSellerByAuthActorId } from '../../../../../../shared/infra/http/utils' +import sellerCustomerGroup from "../../../../../../links/seller-customer-group"; +import sellerProduct from "../../../../../../links/seller-product"; +import "../../../../../../shared/infra/http/middlewares/types"; +import { fetchSellerByAuthActorId } from "../../../../../../shared/infra/http/utils"; +import { getRuleAttributesMap } from "../../../utils"; /** * @oas [get] /vendor/promotions/rule-attribute-options/{rule_type} @@ -78,64 +76,93 @@ import { fetchSellerByAuthActorId } from '../../../../../../shared/infra/http/ut * $ref: "#/components/schemas/VendorRuleAttributeOption" * */ + +const vendorRuleQueryConfigurations = { + ...ruleQueryConfigurations, + product: { + entryPoint: "product", + valueAttr: "id", + labelAttr: "title", + }, +}; + export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); + + const ruleType = req.normalized_rule_type || req.params.rule_type; + const { rule_attribute_id: ruleAttributeId } = req.params; const { - rule_type: ruleType, - rule_attribute_id: ruleAttributeId, promotion_type: promotionType, - application_method_type: applicationMethodType - } = req.params - const queryConfig = ruleQueryConfigurations[ruleAttributeId] - const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) - const filterableFields = req.filterableFields + application_method_type: applicationMethodType, + } = req.query; + const queryConfig = vendorRuleQueryConfigurations[ruleAttributeId]; + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY); + const filterableFields = req.filterableFields; + + if (!queryConfig) { + return res.status(400).json({ + type: "invalid_data", + message: `Invalid rule attribute - ${ruleAttributeId}`, + }); + } if (filterableFields.value) { - filterableFields[queryConfig.valueAttr] = filterableFields.value + filterableFields[queryConfig.valueAttr] = filterableFields.value; - delete filterableFields.value + delete filterableFields.value; } - validateRuleType(ruleType) - validateRuleAttribute({ - promotionType: promotionType as PromotionTypeValues, - ruleType: ruleType as RuleTypeValues, - ruleAttributeId, - applicationMethodType: applicationMethodType as ApplicationMethodTypeValues - }) + const ruleAttributesMap = getRuleAttributesMap({ + promotionType: promotionType as string, + applicationMethodType: applicationMethodType as string, + }); + const validAttributes = ruleAttributesMap[ruleType] || []; + const isValidAttribute = validAttributes.some( + (attr) => attr.id === ruleAttributeId + ); + + if (!isValidAttribute) { + return res.status(400).json({ + type: "invalid_data", + message: `Invalid rule attribute - ${ruleAttributeId}. Valid attributes for ${ruleType}: ${validAttributes.map((a) => a.id).join(", ")}`, + }); + } + + delete filterableFields.promotion_type; + delete filterableFields.application_method_type; const seller = await fetchSellerByAuthActorId( req.auth_context.actor_id, req.scope - ) + ); - if (queryConfig.entryPoint === 'product') { + if (queryConfig.entryPoint === "product") { const { data: products } = await query.graph({ entity: sellerProduct.entryPoint, - fields: ['product_id'], + fields: ["product_id"], filters: { - seller_id: seller.id + seller_id: seller.id, }, - withDeleted: true - }) + withDeleted: true, + }); - filterableFields['id'] = products.map((p) => p.product_id) + filterableFields["id"] = products.map((p) => p.product_id); } - if (queryConfig.entryPoint === 'customer_group') { + if (queryConfig.entryPoint === "customer_group") { const { data: groups } = await query.graph({ entity: sellerCustomerGroup.entryPoint, - fields: ['customer_group_id'], + fields: ["customer_group_id"], filters: { - seller_id: seller.id + seller_id: seller.id, }, - withDeleted: true - }) + withDeleted: true, + }); - filterableFields['id'] = groups.map((p) => p.customer_group_id) + filterableFields["id"] = groups.map((p) => p.customer_group_id); } const { rows } = await remoteQuery( @@ -143,18 +170,18 @@ export const GET = async ( entryPoint: queryConfig.entryPoint, variables: { filters: filterableFields, - ...req.queryConfig.pagination + ...req.queryConfig.pagination, }, - fields: [queryConfig.labelAttr, queryConfig.valueAttr] + fields: [queryConfig.labelAttr, queryConfig.valueAttr], }) - ) + ); const values = rows.map((r) => ({ label: r[queryConfig.labelAttr], - value: r[queryConfig.valueAttr] - })) + value: r[queryConfig.valueAttr], + })); res.json({ - values - }) -} + values, + }); +}; diff --git a/packages/modules/b2c-core/src/api/vendor/promotions/utils.ts b/packages/modules/b2c-core/src/api/vendor/promotions/utils.ts index e27b7e799..6e4d91b06 100644 --- a/packages/modules/b2c-core/src/api/vendor/promotions/utils.ts +++ b/packages/modules/b2c-core/src/api/vendor/promotions/utils.ts @@ -1,137 +1,137 @@ import { ApplicationMethodType, PromotionType, - RuleOperator -} from '@medusajs/framework/utils' + RuleOperator, +} from "@medusajs/framework/utils"; export const operatorsMap = { [RuleOperator.IN]: { id: RuleOperator.IN, value: RuleOperator.IN, - label: 'In' + label: "In", }, [RuleOperator.EQ]: { id: RuleOperator.EQ, value: RuleOperator.EQ, - label: 'Equals' + label: "Equals", }, [RuleOperator.NE]: { id: RuleOperator.NE, value: RuleOperator.NE, - label: 'Not In' - } -} + label: "Not In", + }, +}; export enum DisguisedRule { - APPLY_TO_QUANTITY = 'apply_to_quantity', - BUY_RULES_MIN_QUANTITY = 'buy_rules_min_quantity', - CURRENCY_CODE = 'currency_code' + APPLY_TO_QUANTITY = "apply_to_quantity", + BUY_RULES_MIN_QUANTITY = "buy_rules_min_quantity", + CURRENCY_CODE = "currency_code", } const ruleAttributes = [ { - id: 'customer_group', - value: 'customer.groups.id', - label: 'Customer Group', + id: "customer_group", + value: "customer.groups.id", + label: "Customer Group", required: false, - field_type: 'multiselect', - operators: Object.values(operatorsMap) + field_type: "multiselect", + operators: Object.values(operatorsMap), }, { - id: 'region', - value: 'region.id', - label: 'Region', + id: "region", + value: "region.id", + label: "Region", required: false, - field_type: 'multiselect', - operators: Object.values(operatorsMap) + field_type: "multiselect", + operators: Object.values(operatorsMap), }, { - id: 'country', - value: 'shipping_address.country_code', - label: 'Country', + id: "country", + value: "shipping_address.country_code", + label: "Country", required: false, - field_type: 'multiselect', - operators: Object.values(operatorsMap) + field_type: "multiselect", + operators: Object.values(operatorsMap), }, { - id: 'sales_channel', - value: 'sales_channel_id', - label: 'Sales Channel', + id: "sales_channel", + value: "sales_channel_id", + label: "Sales Channel", required: false, - field_type: 'multiselect', - operators: Object.values(operatorsMap) - } -] + field_type: "multiselect", + operators: Object.values(operatorsMap), + }, +]; const commonAttributes = [ { - id: 'product', - value: 'items.product.id', - label: 'Product', + id: "product", + value: "items.product.id", + label: "Product", required: false, - field_type: 'multiselect', - operators: [operatorsMap['in'], operatorsMap['eq']] - } -] + field_type: "multiselect", + operators: [operatorsMap["in"], operatorsMap["eq"]], + }, +]; const currencyRule = { id: DisguisedRule.CURRENCY_CODE, value: DisguisedRule.CURRENCY_CODE, - label: 'Currency Code', - field_type: 'select', + label: "Currency Code", + field_type: "select", required: true, disguised: true, hydrate: true, - operators: [operatorsMap[RuleOperator.EQ]] -} + operators: [operatorsMap[RuleOperator.EQ]], +}; const buyGetBuyRules = [ { id: DisguisedRule.BUY_RULES_MIN_QUANTITY, value: DisguisedRule.BUY_RULES_MIN_QUANTITY, - label: 'Minimum quantity of items', - field_type: 'number', + label: "Minimum quantity of items", + field_type: "number", required: true, disguised: true, - operators: [operatorsMap[RuleOperator.EQ]] - } -] + operators: [operatorsMap[RuleOperator.EQ]], + }, +]; const buyGetTargetRules = [ { id: DisguisedRule.APPLY_TO_QUANTITY, value: DisguisedRule.APPLY_TO_QUANTITY, - label: 'Quantity of items promotion will apply to', - field_type: 'number', + label: "Quantity of items promotion will apply to", + field_type: "number", required: true, disguised: true, - operators: [operatorsMap[RuleOperator.EQ]] - } -] + operators: [operatorsMap[RuleOperator.EQ]], + }, +]; export const getRuleAttributesMap = ({ promotionType, - applicationMethodType + applicationMethodType, }: { - promotionType?: string - applicationMethodType?: string + promotionType?: string; + applicationMethodType?: string; }) => { const map = { rules: [...ruleAttributes], - 'target-rules': [...commonAttributes], - 'buy-rules': [...commonAttributes] - } + target_rules: [...commonAttributes], + buy_rules: [...commonAttributes], + }; if (applicationMethodType === ApplicationMethodType.FIXED) { - map['rules'].push({ ...currencyRule }) + map["rules"].push({ ...currencyRule }); } else { - map['rules'].push({ ...currencyRule, required: false }) + map["rules"].push({ ...currencyRule, required: false }); } if (promotionType === PromotionType.BUYGET) { - map['buy-rules'].push(...buyGetBuyRules) - map['target-rules'].push(...buyGetTargetRules) + map["buy_rules"].push(...buyGetBuyRules); + map["target_rules"].push(...buyGetTargetRules); } - return map -} + return map; +}; diff --git a/packages/modules/b2c-core/src/api/vendor/promotions/validators.ts b/packages/modules/b2c-core/src/api/vendor/promotions/validators.ts index 8285d9c1f..00fd7f967 100644 --- a/packages/modules/b2c-core/src/api/vendor/promotions/validators.ts +++ b/packages/modules/b2c-core/src/api/vendor/promotions/validators.ts @@ -6,14 +6,7 @@ import { ApplicationMethodType, PromotionStatus, PromotionType, - RuleType, } from "@medusajs/framework/utils"; -import { - ApplicationMethodTargetTypeValues, - ApplicationMethodTypeValues, - PromotionTypeValues, - RuleTypeValues, -} from "@medusajs/types"; import { createFindParams, createSelectParams, @@ -257,6 +250,8 @@ export const VendorGetPromotionsRuleValueParams = createFindParams({ z.object({ q: z.string().optional(), value: z.union([z.string(), z.array(z.string())]).optional(), + promotion_type: z.nativeEnum(PromotionType).optional(), + application_method_type: z.nativeEnum(ApplicationMethodType).optional(), }) ); @@ -278,9 +273,25 @@ export const VendorGetPromotionRuleTypeParams = createSelectParams().merge( }) ); +export const VendorRuleTypePathParam = z + .string() + .refine( + (val) => + [ + "rules", + "target-rules", + "target_rules", + "buy-rules", + "buy_rules", + ].includes(val), + { + message: + "Invalid rule type. Expected 'rules' | 'target-rules' | 'buy-rules'", + } + ) + .transform((val) => val.replace(/-/g, "_")); + export const VendorGetPromotionsRuleValuePathParams = z.object({ - rule_type: z.nativeEnum(RuleType), + rule_type: VendorRuleTypePathParam, rule_attribute_id: z.string(), - promotion_type: z.nativeEnum(PromotionType).optional(), - application_method_type: z.nativeEnum(ApplicationMethodType).optional(), }); diff --git a/packages/modules/b2c-core/src/shared/infra/http/middlewares/types.ts b/packages/modules/b2c-core/src/shared/infra/http/middlewares/types.ts new file mode 100644 index 000000000..81f721105 --- /dev/null +++ b/packages/modules/b2c-core/src/shared/infra/http/middlewares/types.ts @@ -0,0 +1,16 @@ +import { MedusaRequest } from "@medusajs/framework"; + +/** + * Extend MedusaRequest with custom properties used by vendor promotions middleware + */ +declare module "@medusajs/framework" { + interface MedusaRequest { + /** + * Normalized rule_type value set by vendor promotion middleware. + * Converts hyphenated format (target-rules) to underscored format (target_rules) + * to match Medusa's RuleType enum standard. + */ + normalized_rule_type?: string; + } +} + diff --git a/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-attribute-options-path-params-guard.ts b/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-attribute-options-path-params-guard.ts new file mode 100644 index 000000000..5a2431067 --- /dev/null +++ b/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-attribute-options-path-params-guard.ts @@ -0,0 +1,33 @@ +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework"; + +import { VendorRuleTypePathParam } from "../../../../api/vendor/promotions/validators"; +import "./types"; + +export const vendorPromotionsRuleAttributeOptionsPathParamsGuard = async ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction +) => { + const result = VendorRuleTypePathParam.safeParse(req.params.rule_type); + + if (!result.success) { + return res.status(400).json({ + error: "Invalid path parameters", + details: result.error.errors, + }); + } + + req.normalized_rule_type = result.data; + + try { + req.params.rule_type = result.data; + } catch (e) { + // req.params is frozen, normalized_rule_type will be used + } + + next(); +}; diff --git a/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-value-options-path-params-guard.ts b/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-value-options-path-params-guard.ts index d7f51c2e3..d95df8fcc 100644 --- a/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-value-options-path-params-guard.ts +++ b/packages/modules/b2c-core/src/shared/infra/http/middlewares/vendor-promotions-rule-value-options-path-params-guard.ts @@ -1,10 +1,11 @@ import { + MedusaNextFunction, MedusaRequest, MedusaResponse, - MedusaNextFunction, } from "@medusajs/framework"; import { VendorGetPromotionsRuleValuePathParams } from "../../../../api/vendor/promotions/validators"; +import "./types"; export const vendorPromotionsRuleValueOptionsPathParamsGuard = async ( req: MedusaRequest, @@ -18,6 +19,13 @@ export const vendorPromotionsRuleValueOptionsPathParamsGuard = async ( details: result.error.errors, }); } + req.normalized_rule_type = result.data.rule_type; + + try { + req.params.rule_type = result.data.rule_type; + } catch (e) { + // req.params is frozen, normalized_rule_type will be used + } next(); };