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

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

View File

@@ -1,5 +1,5 @@
import { RuleOperator } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { RuleOperator } from "@medusajs/utils"
import {
adminHeaders,
createAdminUser,
@@ -110,9 +110,7 @@ medusaIntegrationTestRunner({
prices: [{ currency_code: "usd", amount: 1000 }],
}
const {
data: { shipping_option: shippingOption },
} = await api.post(
await api.post(
`/admin/shipping-options`,
shippingOptionPayload,
adminHeaders
@@ -147,7 +145,25 @@ medusaIntegrationTestRunner({
description: "Test description",
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 {
@@ -167,7 +183,7 @@ medusaIntegrationTestRunner({
id: expect.any(String),
name: "Test shipping option",
price_type: "flat",
prices: [
prices: expect.arrayContaining([
{
id: expect.any(String),
amount: 1000,
@@ -185,8 +201,39 @@ medusaIntegrationTestRunner({
created_at: expect.any(String),
updated_at: expect.any(String),
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",
@@ -275,6 +322,17 @@ medusaIntegrationTestRunner({
region_id: region.id,
amount: 1000,
},
{
region_id: region.id,
amount: 500,
rules: [
{
attribute: "cart_total",
operator: "gt",
value: 200,
},
],
},
],
rules: [shippingOptionRule],
}
@@ -313,6 +371,24 @@ medusaIntegrationTestRunner({
currency_code: "eur",
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([
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 () => {
const shippingOptionPayload = {
name: "Test shipping option",
@@ -431,6 +621,17 @@ medusaIntegrationTestRunner({
id: eurPrice.id,
amount: 10000,
},
{
currency_code: "dkk",
amount: 5,
rules: [
{
attribute: "cart_total",
operator: "gt",
value: 200,
},
],
},
],
rules: [
{
@@ -463,7 +664,7 @@ medusaIntegrationTestRunner({
)
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).toEqual(
expect.objectContaining({
@@ -494,6 +695,19 @@ medusaIntegrationTestRunner({
rules_count: 1,
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([
expect.objectContaining({

View File

@@ -1,19 +1,23 @@
import {
CreatePriceSetDTO,
CreatePriceSetPriceRules,
IPricingModuleService,
IRegionModuleService,
PriceRule,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { isString, Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export interface ShippingOptionsPriceCurrencyCode {
currency_code: string
amount: number
rules?: PriceRule[]
}
interface ShippingOptionsPriceRegionId {
region_id: string
amount: number
rules?: PriceRule[]
}
export type CreateShippingOptionsPriceSetsStepInput = {
@@ -21,15 +25,36 @@ export type CreateShippingOptionsPriceSetsStepInput = {
prices: (ShippingOptionsPriceCurrencyCode | ShippingOptionsPriceRegionId)[]
}[]
function buildPriceSet(
export function buildPriceSet(
prices: CreateShippingOptionsPriceSetsStepInput[0]["prices"],
regionToCurrencyMap: Map<string, string>
): CreatePriceSetDTO {
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) {
return {
currency_code: price.currency_code,
amount: price.amount,
rules: additionalRules,
}
}
@@ -38,6 +63,7 @@ function buildPriceSet(
amount: price.amount,
rules: {
region_id: price.region_id,
...additionalRules,
},
}
})

View File

@@ -1,5 +1,6 @@
import {
CreatePriceDTO,
CreatePriceSetPriceRules,
CreatePricesDTO,
FulfillmentWorkflow,
IPricingModuleService,
@@ -13,6 +14,7 @@ import {
LINKS,
Modules,
isDefined,
isString,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
@@ -64,6 +66,26 @@ function buildPrices(
}
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) {
const currency_code = regionToCurrencyMap.get(price.region_id!)!
const regionId = price.region_id
@@ -74,6 +96,17 @@ function buildPrices(
amount: price.amount,
rules: {
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,
}
})
const buildPricesData =
pricesData && buildPrices(pricesData, regionToCurrencyMap)
return [
currentShippingOptionDataItem.shipping_option_id,
{

View File

@@ -138,3 +138,9 @@ export interface FilterablePriceRuleProps
* The possible operators to use in a price rule.
*/
export type PricingRuleOperatorValues = "gt" | "lt" | "eq" | "lte" | "gte"
export interface PriceRule {
attribute: string
operator: PricingRuleOperatorValues
value: number
}

View File

@@ -1,5 +1,6 @@
import { ShippingOptionPriceType } from "../../fulfillment"
import { RuleOperatorType } from "../../common"
import { ShippingOptionPriceType } from "../../fulfillment"
import { PriceRule } from "../../pricing"
export interface UpdateShippingOptionsWorkflowInput {
id: string
@@ -19,11 +20,13 @@ export interface UpdateShippingOptionsWorkflowInput {
id?: string
currency_code?: string
amount?: number
rules?: PriceRule[]
}
| {
id?: string
region_id?: string
amount?: number
rules?: PriceRule[]
}
)[]
rules?: {

View File

@@ -10,6 +10,7 @@ export const defaultAdminShippingOptionFields = [
"*rules",
"*type",
"*prices",
"*prices.price_rules",
"*service_zone",
"*shipping_profile",
"*provider",

View File

@@ -1,4 +1,5 @@
import {
PricingRuleOperator,
RuleOperator,
ShippingOptionPriceType as ShippingOptionPriceTypeEnum,
} from "@medusajs/framework/utils"
@@ -83,11 +84,20 @@ export const AdminCreateShippingOptionTypeObject = z
})
.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
export const AdminCreateShippingOptionPriceWithCurrency = z
.object({
currency_code: z.string(),
amount: z.number(),
rules: AdminPriceRules.optional(),
})
.strict()
@@ -95,6 +105,7 @@ export const AdminCreateShippingOptionPriceWithRegion = z
.object({
region_id: z.string(),
amount: z.number(),
rules: AdminPriceRules.optional(),
})
.strict()
@@ -103,6 +114,7 @@ export const AdminUpdateShippingOptionPriceWithCurrency = z
id: z.string().optional(),
currency_code: z.string().optional(),
amount: z.number().optional(),
rules: AdminPriceRules.optional(),
})
.strict()
@@ -111,6 +123,7 @@ export const AdminUpdateShippingOptionPriceWithRegion = z
id: z.string().optional(),
region_id: z.string().optional(),
amount: z.number().optional(),
rules: AdminPriceRules.optional(),
})
.strict()