feat(core-flows,types,medusa): ability to update/create custom shipping prices (#10368)

This commit is contained in:
Riqwan Thamir authored and GitHub committed 2024-11-29 15:04:33 +01:00
1 parent ed11834d6e
commit 5719e825ca
7 files changed
+310 -12

No files matched your search

@@ -1,5 +1,5 @@
import { RuleOperator } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { RuleOperator } from "@medusajs/utils"
import { import {
adminHeaders, adminHeaders,
createAdminUser, createAdminUser,
@@ -110,9 +110,7 @@ medusaIntegrationTestRunner({
prices: [{ currency_code: "usd", amount: 1000 }], prices: [{ currency_code: "usd", amount: 1000 }],
} }
const { await api.post(
data: { shipping_option: shippingOption },
} = await api.post(
`/admin/shipping-options`, `/admin/shipping-options`,
shippingOptionPayload, shippingOptionPayload,
adminHeaders adminHeaders
@@ -147,7 +145,25 @@ medusaIntegrationTestRunner({
description: "Test description", description: "Test description",
code: "test-code", code: "test-code",
}, },
prices: [{ currency_code: "usd", amount: 1000 }], prices: [
{ currency_code: "usd", amount: 1000 },
{
currency_code: "usd",
amount: 500,
rules: [
{
attribute: "cart_total",
operator: "gte",
value: 100,
},
{
attribute: "cart_total",
operator: "lte",
value: 200,
},
],
},
],
} }
const { const {
@@ -167,7 +183,7 @@ medusaIntegrationTestRunner({
id: expect.any(String), id: expect.any(String),
name: "Test shipping option", name: "Test shipping option",
price_type: "flat", price_type: "flat",
prices: [ prices: expect.arrayContaining([
{ {
id: expect.any(String), id: expect.any(String),
amount: 1000, amount: 1000,
@@ -185,8 +201,39 @@ medusaIntegrationTestRunner({
created_at: expect.any(String), created_at: expect.any(String),
updated_at: expect.any(String), updated_at: expect.any(String),
deleted_at: null, deleted_at: null,
price_rules: [],
}, },
], {
id: expect.any(String),
amount: 500,
currency_code: "usd",
max_quantity: null,
min_quantity: null,
price_list: null,
price_set_id: expect.any(String),
raw_amount: {
precision: 20,
value: "500",
},
rules_count: 2,
price_rules: expect.arrayContaining([
expect.objectContaining({
attribute: "cart_total",
operator: "gte",
value: "100",
}),
expect.objectContaining({
attribute: "cart_total",
operator: "lte",
value: "200",
}),
]),
title: null,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
},
]),
provider_id: "manual_test-provider", provider_id: "manual_test-provider",
provider: { provider: {
id: "manual_test-provider", id: "manual_test-provider",
@@ -275,6 +322,17 @@ medusaIntegrationTestRunner({
region_id: region.id, region_id: region.id,
amount: 1000, amount: 1000,
}, },
{
region_id: region.id,
amount: 500,
rules: [
{
attribute: "cart_total",
operator: "gt",
value: 200,
},
],
},
], ],
rules: [shippingOptionRule], rules: [shippingOptionRule],
} }
@@ -313,6 +371,24 @@ medusaIntegrationTestRunner({
currency_code: "eur", currency_code: "eur",
amount: 1000, amount: 1000,
}), }),
expect.objectContaining({
id: expect.any(String),
currency_code: "eur",
amount: 500,
rules_count: 2,
price_rules: expect.arrayContaining([
expect.objectContaining({
attribute: "cart_total",
operator: "gt",
value: "200",
}),
expect.objectContaining({
attribute: "region_id",
operator: "eq",
value: region.id,
}),
]),
}),
]), ]),
rules: expect.arrayContaining([ rules: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -326,6 +402,120 @@ medusaIntegrationTestRunner({
) )
}) })
it("should throw error when creating a price rule with a non white listed attribute", async () => {
const shippingOptionPayload = {
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
prices: [
{
currency_code: "usd",
amount: 500,
rules: [
{
attribute: "not_whitelisted",
operator: "gte",
value: 100,
},
],
},
],
}
const error = await api
.post(
`/admin/shipping-options`,
shippingOptionPayload,
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
})
it("should throw error when creating a price rule with a non white listed operator", async () => {
const shippingOptionPayload = {
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
prices: [
{
currency_code: "usd",
amount: 500,
rules: [
{
attribute: "cart_total",
operator: "not_whitelisted",
value: 100,
},
],
},
],
}
const error = await api
.post(
`/admin/shipping-options`,
shippingOptionPayload,
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
})
it("should throw error when creating a price rule with a string value", async () => {
const shippingOptionPayload = {
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
prices: [
{
currency_code: "usd",
amount: 500,
rules: [
{
attribute: "cart_total",
operator: "gt",
value: "string",
},
],
},
],
}
const error = await api
.post(
`/admin/shipping-options`,
shippingOptionPayload,
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
})
it("should throw error when provider does not exist on a location", async () => { it("should throw error when provider does not exist on a location", async () => {
const shippingOptionPayload = { const shippingOptionPayload = {
name: "Test shipping option", name: "Test shipping option",
@@ -431,6 +621,17 @@ medusaIntegrationTestRunner({
id: eurPrice.id, id: eurPrice.id,
amount: 10000, amount: 10000,
}, },
{
currency_code: "dkk",
amount: 5,
rules: [
{
attribute: "cart_total",
operator: "gt",
value: 200,
},
],
},
], ],
rules: [ rules: [
{ {
@@ -463,7 +664,7 @@ medusaIntegrationTestRunner({
) )
expect(updateResponse.status).toEqual(200) expect(updateResponse.status).toEqual(200)
expect(updateResponse.data.shipping_option.prices).toHaveLength(2) expect(updateResponse.data.shipping_option.prices).toHaveLength(3)
expect(updateResponse.data.shipping_option.rules).toHaveLength(3) expect(updateResponse.data.shipping_option.rules).toHaveLength(3)
expect(updateResponse.data.shipping_option).toEqual( expect(updateResponse.data.shipping_option).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -494,6 +695,19 @@ medusaIntegrationTestRunner({
rules_count: 1, rules_count: 1,
amount: 10000, amount: 10000,
}), }),
expect.objectContaining({
id: expect.any(String),
currency_code: "dkk",
rules_count: 1,
amount: 5,
price_rules: [
expect.objectContaining({
attribute: "cart_total",
operator: "gt",
value: "200",
}),
],
}),
]), ]),
rules: expect.arrayContaining([ rules: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -1,19 +1,23 @@
import { import {
CreatePriceSetDTO, CreatePriceSetDTO,
CreatePriceSetPriceRules,
IPricingModuleService, IPricingModuleService,
IRegionModuleService, IRegionModuleService,
PriceRule,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils" import { isString, Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export interface ShippingOptionsPriceCurrencyCode { export interface ShippingOptionsPriceCurrencyCode {
currency_code: string currency_code: string
amount: number amount: number
rules?: PriceRule[]
} }
interface ShippingOptionsPriceRegionId { interface ShippingOptionsPriceRegionId {
region_id: string region_id: string
amount: number amount: number
rules?: PriceRule[]
} }
export type CreateShippingOptionsPriceSetsStepInput = { export type CreateShippingOptionsPriceSetsStepInput = {
@@ -21,15 +25,36 @@ export type CreateShippingOptionsPriceSetsStepInput = {
prices: (ShippingOptionsPriceCurrencyCode | ShippingOptionsPriceRegionId)[] prices: (ShippingOptionsPriceCurrencyCode | ShippingOptionsPriceRegionId)[]
}[] }[]
function buildPriceSet( export function buildPriceSet(
prices: CreateShippingOptionsPriceSetsStepInput[0]["prices"], prices: CreateShippingOptionsPriceSetsStepInput[0]["prices"],
regionToCurrencyMap: Map<string, string> regionToCurrencyMap: Map<string, string>
): CreatePriceSetDTO { ): CreatePriceSetDTO {
const shippingOptionPrices = prices.map((price) => { const shippingOptionPrices = prices.map((price) => {
const { rules = [] } = price
const additionalRules: CreatePriceSetPriceRules = {}
for (const rule of rules) {
let existingPriceRules = additionalRules[rule.attribute]
if (isString(existingPriceRules)) {
continue
}
existingPriceRules ||= []
existingPriceRules.push({
operator: rule.operator,
value: rule.value,
})
additionalRules[rule.attribute] = existingPriceRules
}
if ("currency_code" in price) { if ("currency_code" in price) {
return { return {
currency_code: price.currency_code, currency_code: price.currency_code,
amount: price.amount, amount: price.amount,
rules: additionalRules,
} }
} }
@@ -38,6 +63,7 @@ function buildPriceSet(
amount: price.amount, amount: price.amount,
rules: { rules: {
region_id: price.region_id, region_id: price.region_id,
...additionalRules,
}, },
} }
}) })
@@ -1,5 +1,6 @@
import { import {
CreatePriceDTO, CreatePriceDTO,
CreatePriceSetPriceRules,
CreatePricesDTO, CreatePricesDTO,
FulfillmentWorkflow, FulfillmentWorkflow,
IPricingModuleService, IPricingModuleService,
@@ -13,6 +14,7 @@ import {
LINKS, LINKS,
Modules, Modules,
isDefined, isDefined,
isString,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
@@ -64,6 +66,26 @@ function buildPrices(
} }
const shippingOptionPrices = prices.map((price) => { const shippingOptionPrices = prices.map((price) => {
const { rules = [] } = price
const additionalRules: CreatePriceSetPriceRules = {}
for (const rule of rules) {
let existingPriceRules = additionalRules[rule.attribute]
if (isString(existingPriceRules)) {
continue
}
existingPriceRules ||= []
existingPriceRules.push({
operator: rule.operator,
value: rule.value,
})
additionalRules[rule.attribute] = existingPriceRules
}
if ("region_id" in price) { if ("region_id" in price) {
const currency_code = regionToCurrencyMap.get(price.region_id!)! const currency_code = regionToCurrencyMap.get(price.region_id!)!
const regionId = price.region_id const regionId = price.region_id
@@ -74,6 +96,17 @@ function buildPrices(
amount: price.amount, amount: price.amount,
rules: { rules: {
region_id: regionId, region_id: regionId,
...additionalRules,
},
}
}
if ("currency_code" in price) {
return {
...price,
amount: price.amount,
rules: {
...additionalRules,
}, },
} }
} }
@@ -142,8 +175,10 @@ export const setShippingOptionsPricesStep = createStep(
price_set_id: currentShippingOptionDataItem.price_set_id, price_set_id: currentShippingOptionDataItem.price_set_id,
} }
}) })
const buildPricesData = const buildPricesData =
pricesData && buildPrices(pricesData, regionToCurrencyMap) pricesData && buildPrices(pricesData, regionToCurrencyMap)
return [ return [
currentShippingOptionDataItem.shipping_option_id, currentShippingOptionDataItem.shipping_option_id,
{ {
@@ -138,3 +138,9 @@ export interface FilterablePriceRuleProps
* The possible operators to use in a price rule. * The possible operators to use in a price rule.
*/ */
export type PricingRuleOperatorValues = "gt" | "lt" | "eq" | "lte" | "gte" export type PricingRuleOperatorValues = "gt" | "lt" | "eq" | "lte" | "gte"
export interface PriceRule {
attribute: string
operator: PricingRuleOperatorValues
value: number
}
@@ -1,5 +1,6 @@
import { ShippingOptionPriceType } from "../../fulfillment"
import { RuleOperatorType } from "../../common" import { RuleOperatorType } from "../../common"
import { ShippingOptionPriceType } from "../../fulfillment"
import { PriceRule } from "../../pricing"
export interface UpdateShippingOptionsWorkflowInput { export interface UpdateShippingOptionsWorkflowInput {
id: string id: string
@@ -19,11 +20,13 @@ export interface UpdateShippingOptionsWorkflowInput {
id?: string id?: string
currency_code?: string currency_code?: string
amount?: number amount?: number
rules?: PriceRule[]
} }
| { | {
id?: string id?: string
region_id?: string region_id?: string
amount?: number amount?: number
rules?: PriceRule[]
} }
)[] )[]
rules?: { rules?: {
@@ -10,6 +10,7 @@ export const defaultAdminShippingOptionFields = [
"*rules", "*rules",
"*type", "*type",
"*prices", "*prices",
"*prices.price_rules",
"*service_zone", "*service_zone",
"*shipping_profile", "*shipping_profile",
"*provider", "*provider",
@@ -1,4 +1,5 @@
import { import {
PricingRuleOperator,
RuleOperator, RuleOperator,
ShippingOptionPriceType as ShippingOptionPriceTypeEnum, ShippingOptionPriceType as ShippingOptionPriceTypeEnum,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
@@ -83,11 +84,20 @@ export const AdminCreateShippingOptionTypeObject = z
}) })
.strict() .strict()
const AdminPriceRules = z.array(
z.object({
attribute: z.literal("cart_total"),
operator: z.nativeEnum(PricingRuleOperator),
value: z.number(),
})
)
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export const AdminCreateShippingOptionPriceWithCurrency = z export const AdminCreateShippingOptionPriceWithCurrency = z
.object({ .object({
currency_code: z.string(), currency_code: z.string(),
amount: z.number(), amount: z.number(),
rules: AdminPriceRules.optional(),
}) })
.strict() .strict()
@@ -95,6 +105,7 @@ export const AdminCreateShippingOptionPriceWithRegion = z
.object({ .object({
region_id: z.string(), region_id: z.string(),
amount: z.number(), amount: z.number(),
rules: AdminPriceRules.optional(),
}) })
.strict() .strict()
@@ -103,6 +114,7 @@ export const AdminUpdateShippingOptionPriceWithCurrency = z
id: z.string().optional(), id: z.string().optional(),
currency_code: z.string().optional(), currency_code: z.string().optional(),
amount: z.number().optional(), amount: z.number().optional(),
rules: AdminPriceRules.optional(),
}) })
.strict() .strict()
@@ -111,6 +123,7 @@ export const AdminUpdateShippingOptionPriceWithRegion = z
id: z.string().optional(), id: z.string().optional(),
region_id: z.string().optional(), region_id: z.string().optional(),
amount: z.number().optional(), amount: z.number().optional(),
rules: AdminPriceRules.optional(),
}) })
.strict() .strict()