feat(medusa): Update shipping option price type (#5895)

**What**
- allow updates to shipping option price type
- Clean up the use of strings in the api validation

Fixes #5824
This commit is contained in:
Philip Korsholm authored and GitHub committed 2023-12-15 10:12:43 +00:00
1 parent 761d2e7a69
commit 56b07ed0cf
7 files changed
+143 -117

No files matched your search

@@ -48,7 +48,7 @@ describe("/admin/shipping-options", () => {
await db.teardown() await db.teardown()
}) })
it("updates a shipping option with no existing requirements", async () => { it("should update a shipping option with no existing requirements", async () => {
const api = useApi() const api = useApi()
const payload = { const payload = {
@@ -66,11 +66,11 @@ describe("/admin/shipping-options", () => {
], ],
} }
const res = await api.post(`/admin/shipping-options/test-out`, payload, { const res = await api.post(
headers: { `/admin/shipping-options/test-out`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
const requirements = res.data.shipping_option.requirements const requirements = res.data.shipping_option.requirements
@@ -92,7 +92,27 @@ describe("/admin/shipping-options", () => {
) )
}) })
it("fails to add a a requirement with an id if it does not exists", async () => { it("should update a shipping option with price_type", async () => {
const api = useApi()
const payload = {
price_type: "calculated",
requirements: [],
}
const res = await api
.post(`/admin/shipping-options/test-out`, payload, adminReqConfig)
.catch(console.log)
expect(res.status).toEqual(200)
expect(res.data.shipping_option).toEqual(
expect.objectContaining({
price_type: "calculated",
})
)
})
it("should fail to add a a requirement with an id if it does not exists", async () => {
const api = useApi() const api = useApi()
const payload = { const payload = {
@@ -113,11 +133,7 @@ describe("/admin/shipping-options", () => {
} }
const res = await api const res = await api
.post(`/admin/shipping-options/test-out`, payload, { .post(`/admin/shipping-options/test-out`, payload, adminReqConfig)
headers: {
"x-medusa-access-token": "test_token",
},
})
.catch((err) => { .catch((err) => {
return err.response return err.response
}) })
@@ -128,7 +144,7 @@ describe("/admin/shipping-options", () => {
) )
}) })
it("it successfully updates a set of existing requirements", async () => { it("should successfully updates a set of existing requirements", async () => {
const api = useApi() const api = useApi()
const payload = { const payload = {
@@ -148,11 +164,11 @@ describe("/admin/shipping-options", () => {
} }
const res = await api const res = await api
.post(`/admin/shipping-options/test-option-req`, payload, { .post(
headers: { `/admin/shipping-options/test-option-req`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
.catch((err) => { .catch((err) => {
console.log(err.response.data.message) console.log(err.response.data.message)
}) })
@@ -160,7 +176,7 @@ describe("/admin/shipping-options", () => {
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
}) })
it("it successfully updates a set of existing requirements by updating one and deleting the other", async () => { it("should successfully updates a set of existing requirements by updating one and deleting the other", async () => {
const api = useApi() const api = useApi()
const payload = { const payload = {
@@ -174,11 +190,11 @@ describe("/admin/shipping-options", () => {
} }
const res = await api const res = await api
.post(`/admin/shipping-options/test-option-req`, payload, { .post(
headers: { `/admin/shipping-options/test-option-req`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
.catch((err) => { .catch((err) => {
console.log(err.response.data.message) console.log(err.response.data.message)
}) })
@@ -186,7 +202,7 @@ describe("/admin/shipping-options", () => {
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
}) })
it("successfully updates a set of requirements because max. subtotal >= min. subtotal", async () => { it("should successfully updates a set of requirements because max. subtotal >= min. subtotal", async () => {
const api = useApi() const api = useApi()
const payload = { const payload = {
@@ -205,11 +221,11 @@ describe("/admin/shipping-options", () => {
} }
const res = await api const res = await api
.post(`/admin/shipping-options/test-option-req`, payload, { .post(
headers: { `/admin/shipping-options/test-option-req`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
.catch((err) => { .catch((err) => {
console.log(err.response.data.message) console.log(err.response.data.message)
}) })
@@ -219,7 +235,7 @@ describe("/admin/shipping-options", () => {
expect(res.data.shipping_option.requirements[1].amount).toEqual(200) expect(res.data.shipping_option.requirements[1].amount).toEqual(200)
}) })
it("fails to updates a set of requirements because max. subtotal <= min. subtotal", async () => { it("should fail to updates a set of requirements because max. subtotal <= min. subtotal", async () => {
const api = useApi() const api = useApi()
const payload = { const payload = {
@@ -238,11 +254,11 @@ describe("/admin/shipping-options", () => {
} }
const res = await api const res = await api
.post(`/admin/shipping-options/test-option-req`, payload, { .post(
headers: { `/admin/shipping-options/test-option-req`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
.catch((err) => { .catch((err) => {
return err.response return err.response
}) })
@@ -267,11 +283,7 @@ describe("/admin/shipping-options", () => {
{ {
fulfillment_providers: ["test-ful"], fulfillment_providers: ["test-ful"],
}, },
{ adminReqConfig
headers: {
"x-medusa-access-token": "test_token",
},
}
) )
const manager = dbConnection.manager const manager = dbConnection.manager
@@ -297,7 +309,7 @@ describe("/admin/shipping-options", () => {
await db.teardown() await db.teardown()
}) })
it("creates a shipping option with requirements", async () => { it("should create a shipping option with requirements", async () => {
const api = useApi() const api = useApi()
payload.requirements = [ payload.requirements = [
{ {
@@ -310,29 +322,29 @@ describe("/admin/shipping-options", () => {
}, },
] ]
const res = await api.post(`/admin/shipping-options`, payload, { const res = await api.post(
headers: { `/admin/shipping-options`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
expect(res.data.shipping_option.requirements.length).toEqual(2) expect(res.data.shipping_option.requirements.length).toEqual(2)
}) })
it("creates a shipping option with no requirements", async () => { it("should create a shipping option with no requirements", async () => {
const api = useApi() const api = useApi()
const res = await api.post(`/admin/shipping-options`, payload, { const res = await api.post(
headers: { `/admin/shipping-options`,
"x-medusa-access-token": "test_token", payload,
}, adminReqConfig
}) )
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
expect(res.data.shipping_option.requirements.length).toEqual(0) expect(res.data.shipping_option.requirements.length).toEqual(0)
}) })
it("fails on same requirement types", async () => { it("should fail on same requirement types", async () => {
const api = useApi() const api = useApi()
payload.requirements = [ payload.requirements = [
{ {
@@ -346,11 +358,7 @@ describe("/admin/shipping-options", () => {
] ]
try { try {
await api.post(`/admin/shipping-options`, payload, { await api.post(`/admin/shipping-options`, payload, adminReqConfig)
headers: {
"x-medusa-access-token": "test_token",
},
})
} catch (error) { } catch (error) {
expect(error.response.data.message).toEqual( expect(error.response.data.message).toEqual(
"Only one requirement of each type is allowed" "Only one requirement of each type is allowed"
@@ -358,7 +366,7 @@ describe("/admin/shipping-options", () => {
} }
}) })
it("fails when min_subtotal > max_subtotal", async () => { it("should fail when min_subtotal > max_subtotal", async () => {
const api = useApi() const api = useApi()
payload.requirements = [ payload.requirements = [
{ {
@@ -372,11 +380,7 @@ describe("/admin/shipping-options", () => {
] ]
try { try {
await api.post(`/admin/shipping-options`, payload, { await api.post(`/admin/shipping-options`, payload, adminReqConfig)
headers: {
"x-medusa-access-token": "test_token",
},
})
} catch (error) { } catch (error) {
expect(error.response.data.message).toEqual( expect(error.response.data.message).toEqual(
"Max. subtotal must be greater than Min. subtotal" "Max. subtotal must be greater than Min. subtotal"
@@ -395,24 +399,19 @@ describe("/admin/shipping-options", () => {
await db.teardown() await db.teardown()
}) })
it("lists shipping options", async () => { it("should list shipping options", async () => {
const api = useApi() const api = useApi()
const res = await api.get(`/admin/shipping-options`, { const res = await api.get(`/admin/shipping-options`, adminReqConfig)
headers: {
"x-medusa-access-token": "test_token",
},
})
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
}) })
it("lists admin only shipping options", async () => { it("should list admin only shipping options", async () => {
const api = useApi() const api = useApi()
const res = await api.get(`/admin/shipping-options?admin_only=true`, { const res = await api.get(
headers: { `/admin/shipping-options?admin_only=true`,
"x-medusa-access-token": "test_token", adminReqConfig
}, )
})
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
expect(res.data.shipping_options).toEqual( expect(res.data.shipping_options).toEqual(
@@ -425,13 +424,12 @@ describe("/admin/shipping-options", () => {
) )
}) })
it("lists return shipping options", async () => { it("should list return shipping options", async () => {
const api = useApi() const api = useApi()
const res = await api.get(`/admin/shipping-options?is_return=true`, { const res = await api.get(
headers: { `/admin/shipping-options?is_return=true`,
"x-medusa-access-token": "test_token", adminReqConfig
}, )
})
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
expect(res.data.shipping_options).toEqual( expect(res.data.shipping_options).toEqual(
@@ -444,15 +442,11 @@ describe("/admin/shipping-options", () => {
) )
}) })
it("lists shipping options without return and admin options", async () => { it("should list shipping options without return and admin options", async () => {
const api = useApi() const api = useApi()
const res = await api.get( const res = await api.get(
`/admin/shipping-options?is_return=false&admin_only=true`, `/admin/shipping-options?is_return=false&admin_only=true`,
{ adminReqConfig
headers: {
"x-medusa-access-token": "test_token",
},
}
) )
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
@@ -1,6 +1,6 @@
import { IdMap } from "medusa-test-utils" import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ShippingOptionServiceMock } from "../../../../../services/__mocks__/shipping-option" import { ShippingOptionServiceMock } from "../../../../../services/__mocks__/shipping-option"
import { request } from "../../../../../helpers/test-request"
describe("POST /admin/shipping-options", () => { describe("POST /admin/shipping-options", () => {
describe("successful creation", () => { describe("successful creation", () => {
@@ -1,19 +1,23 @@
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsEnum,
IsNumber, IsNumber,
IsObject, IsObject,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested, ValidateNested,
} from "class-validator" } from "class-validator"
import { RequirementType, ShippingOptionPriceType } from "../../../../models"
import { defaultFields, defaultRelations } from "." import { defaultFields, defaultRelations } from "."
import { Type } from "class-transformer"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { ShippingOptionService } from "../../../../services"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { Type } from "class-transformer"
import { validator } from "../../../../utils/validator" import { validator } from "../../../../utils/validator"
import { CreateShippingOptionInput } from "../../../../types/shipping-options"
/** /**
* @oas [post] /admin/shipping-options * @oas [post] /admin/shipping-options
@@ -88,7 +92,9 @@ import { validator } from "../../../../utils/validator"
export default async (req, res) => { export default async (req, res) => {
const validated = await validator(AdminPostShippingOptionsReq, req.body) const validated = await validator(AdminPostShippingOptionsReq, req.body)
const optionService = req.scope.resolve("shippingOptionService") const optionService: ShippingOptionService = req.scope.resolve(
"shippingOptionService"
)
const shippingProfileService = req.scope.resolve("shippingProfileService") const shippingProfileService = req.scope.resolve("shippingProfileService")
// Add to default shipping profile // Add to default shipping profile
@@ -101,7 +107,7 @@ export default async (req, res) => {
const result = await manager.transaction(async (transactionManager) => { const result = await manager.transaction(async (transactionManager) => {
return await optionService return await optionService
.withTransaction(transactionManager) .withTransaction(transactionManager)
.create(validated) .create(validated as CreateShippingOptionInput)
}) })
const data = await optionService.retrieve(result.id, { const data = await optionService.retrieve(result.id, {
@@ -113,8 +119,11 @@ export default async (req, res) => {
} }
class OptionRequirement { class OptionRequirement {
@IsString() @IsEnum(RequirementType, {
type: string message: `Invalid option type, must be one of "min_subtotal" or "max_subtotal"`,
})
type: RequirementType
@IsNumber() @IsNumber()
amount: number amount: number
} }
@@ -203,15 +212,17 @@ export class AdminPostShippingOptionsReq {
@IsString() @IsString()
provider_id: string provider_id: string
@IsOptional()
@IsString() @IsString()
@IsOptional()
profile_id?: string profile_id?: string
@IsObject() @IsObject()
data: object data: Record<string, unknown>
@IsString() @IsEnum(ShippingOptionPriceType, {
price_type: string message: `Invalid price type, must be one of "flat_rate" or "calculated"`,
})
price_type: ShippingOptionPriceType
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@@ -1,6 +1,7 @@
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsEnum,
IsNumber, IsNumber,
IsObject, IsObject,
IsOptional, IsOptional,
@@ -9,13 +10,14 @@ import {
} from "class-validator" } from "class-validator"
import { defaultFields, defaultRelations } from "." import { defaultFields, defaultRelations } from "."
import { Type } from "class-transformer"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { validator } from "../../../../utils/validator" import { ShippingOptionPriceType } from "../../../../models"
import { ShippingOptionService } from "../../../../services" import { ShippingOptionService } from "../../../../services"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { Type } from "class-transformer"
import { UpdateShippingOptionInput } from "../../../../types/shipping-options" import { UpdateShippingOptionInput } from "../../../../types/shipping-options"
import { validator } from "../../../../utils/validator"
/** /**
* @oas [post] /admin/shipping-options/{id} * @oas [post] /admin/shipping-options/{id}
@@ -123,8 +125,10 @@ class OptionRequirement {
@IsString() @IsString()
@IsOptional() @IsOptional()
id: string id: string
@IsString() @IsString()
type: string type: string
@IsNumber() @IsNumber()
amount: number amount: number
} }
@@ -187,6 +191,12 @@ export class AdminPostShippingOptionsOptionReq {
@IsOptional() @IsOptional()
amount?: number amount?: number
@IsEnum(ShippingOptionPriceType, {
message: `Invalid price type, must be one of "flat_rate" or "calculated"`,
})
@IsOptional()
price_type?: ShippingOptionPriceType
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => OptionRequirement) @Type(() => OptionRequirement)
@@ -15,7 +15,7 @@ import { generateEntityId } from "../utils/generate-entity-id"
/** /**
* @enum * @enum
* *
* The type of shipping option requirement. * The type of shipping option requirement.
*/ */
export enum RequirementType { export enum RequirementType {
+12 -10
View File
@@ -1,8 +1,3 @@
import { FlagRouter, promiseAll } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import { import {
Cart, Cart,
Order, Order,
@@ -11,20 +6,27 @@ import {
ShippingOptionPriceType, ShippingOptionPriceType,
ShippingOptionRequirement, ShippingOptionRequirement,
} from "../models" } from "../models"
import { ShippingMethodRepository } from "../repositories/shipping-method"
import { ShippingOptionRepository } from "../repositories/shipping-option"
import { ShippingOptionRequirementRepository } from "../repositories/shipping-option-requirement"
import { FindConfig, Selector } from "../types/common"
import { import {
CreateShippingMethodDto, CreateShippingMethodDto,
CreateShippingOptionInput, CreateShippingOptionInput,
ShippingMethodUpdate, ShippingMethodUpdate,
UpdateShippingOptionInput, UpdateShippingOptionInput,
ValidatePriceTypeAndAmountInput, ValidatePriceTypeAndAmountInput,
ValidateRequirementTypeInput,
} from "../types/shipping-options" } from "../types/shipping-options"
import { FindConfig, Selector } from "../types/common"
import { FlagRouter, promiseAll } from "@medusajs/utils"
import { MedusaError, isDefined } from "medusa-core-utils"
import { buildQuery, isString, setMetadata } from "../utils" import { buildQuery, isString, setMetadata } from "../utils"
import { EntityManager } from "typeorm"
import FulfillmentProviderService from "./fulfillment-provider" import FulfillmentProviderService from "./fulfillment-provider"
import RegionService from "./region" import RegionService from "./region"
import { ShippingMethodRepository } from "../repositories/shipping-method"
import { ShippingOptionRepository } from "../repositories/shipping-option"
import { ShippingOptionRequirementRepository } from "../repositories/shipping-option-requirement"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import { TransactionBaseService } from "../interfaces"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
@@ -75,7 +77,7 @@ class ShippingOptionService extends TransactionBaseService {
* @return {ShippingOptionRequirement} a validated shipping requirement * @return {ShippingOptionRequirement} a validated shipping requirement
*/ */
async validateRequirement_( async validateRequirement_(
requirement: ShippingOptionRequirement, requirement: ValidateRequirementTypeInput,
optionId: string | undefined = undefined optionId: string | undefined = undefined
): Promise<ShippingOptionRequirement> { ): Promise<ShippingOptionRequirement> {
return await this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager) => {
+12 -3
View File
@@ -1,10 +1,11 @@
import { Cart, Order } from ".." import { Cart, Order } from ".."
import { ShippingOptionPriceType } from "../models/shipping-option"
import { import {
RequirementType, RequirementType,
ShippingOptionRequirement, ShippingOptionRequirement,
} from "../models/shipping-option-requirement" } from "../models/shipping-option-requirement"
import { ShippingOptionPriceType } from "../models/shipping-option"
export type ShippingRequirement = { export type ShippingRequirement = {
type: RequirementType type: RequirementType
amount: number amount: number
@@ -50,7 +51,15 @@ export type CreateShippingOptionInput = {
is_return?: boolean is_return?: boolean
admin_only?: boolean admin_only?: boolean
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
requirements?: ShippingOptionRequirement[] requirements?: RequirementInput[]
}
export type RequirementInput = { type: RequirementType; amount: number }
export type ValidateRequirementTypeInput = {
id?: string
type: RequirementType
amount: number
} }
export type CreateCustomShippingOptionInput = { export type CreateCustomShippingOptionInput = {
@@ -67,7 +76,7 @@ export type UpdateShippingOptionInput = {
name?: string name?: string
admin_only?: boolean admin_only?: boolean
is_return?: boolean is_return?: boolean
requirements?: ShippingOptionRequirement[] requirements?: RequirementInput[]
region_id?: string region_id?: string
provider_id?: string provider_id?: string
profile_id?: string profile_id?: string