feat(cart): Shipping methods (#6101)

This commit is contained in:
Oli Juhl
2024-01-18 12:16:15 +01:00
committed by GitHub
parent 5cfb662ec0
commit 7f7cb2a263
19 changed files with 431 additions and 31 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/types": patch
---
feat(cart): Shipping methods
@@ -1,4 +1,5 @@
import { ICartModuleService } from "@medusajs/types" import { ICartModuleService } from "@medusajs/types"
import { CheckConstraintViolationException } from "@mikro-orm/core"
import { initialize } from "../../../../src/initialize" import { initialize } from "../../../../src/initialize"
import { DB_URL, MikroOrmWrapper } from "../../../utils" import { DB_URL, MikroOrmWrapper } from "../../../utils"
@@ -466,12 +467,9 @@ describe("Cart Module Service", () => {
expect(item.title).toBe("test") expect(item.title).toBe("test")
const updatedItem = await service.updateLineItems( const updatedItem = await service.updateLineItems(item.id, {
item.id, title: "test2",
{ })
title: "test2",
}
)
expect(updatedItem.title).toBe("test2") expect(updatedItem.title).toBe("test2")
}) })
@@ -519,13 +517,13 @@ describe("Cart Module Service", () => {
selector: { cart_id: createdCart.id }, selector: { cart_id: createdCart.id },
data: { data: {
title: "changed-test", title: "changed-test",
} },
}, },
{ {
selector: { id: itemTwo!.id }, selector: { id: itemTwo!.id },
data: { data: {
title: "changed-other-test", title: "changed-other-test",
} },
}, },
]) ])
@@ -603,4 +601,117 @@ describe("Cart Module Service", () => {
expect(cart.items?.length).toBe(0) expect(cart.items?.length).toBe(0)
}) })
}) })
describe("addShippingMethods", () => {
it("should add a shipping method to cart succesfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [method] = await service.addShippingMethods(createdCart.id, [
{
amount: 100,
name: "Test",
},
])
const cart = await service.retrieve(createdCart.id, {
relations: ["shipping_methods"],
})
expect(method.id).toBe(cart.shipping_methods![0].id)
})
it("should throw when amount is negative", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const error = await service
.addShippingMethods(createdCart.id, [
{
amount: -100,
name: "Test",
},
])
.catch((e) => e)
expect(error.name).toBe(CheckConstraintViolationException.name)
})
it("should add multiple shipping methods to multiple carts succesfully", async () => {
let [eurCart] = await service.create([
{
currency_code: "eur",
},
])
let [usdCart] = await service.create([
{
currency_code: "usd",
},
])
const methods = await service.addShippingMethods([
{
cart_id: eurCart.id,
amount: 100,
name: "Test One",
},
{
cart_id: usdCart.id,
amount: 100,
name: "Test One",
},
])
const carts = await service.list(
{ id: [eurCart.id, usdCart.id] },
{ relations: ["shipping_methods"] }
)
eurCart = carts.find((c) => c.currency_code === "eur")!
usdCart = carts.find((c) => c.currency_code === "usd")!
const eurMethods = methods.filter((m) => m.cart_id === eurCart.id)
const usdMethods = methods.filter((m) => m.cart_id === usdCart.id)
expect(eurCart.shipping_methods![0].id).toBe(eurMethods[0].id)
expect(usdCart.shipping_methods![0].id).toBe(usdMethods[0].id)
expect(eurCart.shipping_methods?.length).toBe(1)
expect(usdCart.shipping_methods?.length).toBe(1)
})
})
describe("removeShippingMethods", () => {
it("should remove a line item succesfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [method] = await service.addShippingMethods(createdCart.id, [
{
amount: 100,
name: "test",
},
])
expect(method.id).not.toBe(null)
await service.removeShippingMethods(method.id)
const cart = await service.retrieve(createdCart.id, {
relations: ["shipping_methods"],
})
expect(cart.shipping_methods?.length).toBe(0)
})
})
}) })
+12 -2
View File
@@ -20,6 +20,9 @@ export default async ({
container.register({ container.register({
cartService: asClass(defaultServices.CartService).singleton(), cartService: asClass(defaultServices.CartService).singleton(),
addressService: asClass(defaultServices.AddressService).singleton(), addressService: asClass(defaultServices.AddressService).singleton(),
shippingMethodService: asClass(
defaultServices.ShippingMethodService
).singleton(),
lineItemService: asClass(defaultServices.LineItemService).singleton(), lineItemService: asClass(defaultServices.LineItemService).singleton(),
}) })
@@ -38,7 +41,14 @@ function loadDefaultRepositories({ container }) {
container.register({ container.register({
baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), baseRepository: asClass(defaultRepositories.BaseRepository).singleton(),
cartRepository: asClass(defaultRepositories.CartRepository).singleton(), cartRepository: asClass(defaultRepositories.CartRepository).singleton(),
addressRepository: asClass(defaultRepositories.AddressRepository).singleton(), addressRepository: asClass(
lineItemRepository: asClass(defaultRepositories.LineItemRepository).singleton(), defaultRepositories.AddressRepository
).singleton(),
lineItemRepository: asClass(
defaultRepositories.LineItemRepository
).singleton(),
shippingMethodRepository: asClass(
defaultRepositories.ShippingMethodRepository
).singleton(),
}) })
} }
+3 -3
View File
@@ -94,13 +94,13 @@ export default class LineItem {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
variant_option_values?: Record<string, unknown> | null variant_option_values?: Record<string, unknown> | null
@Property({ columnType: "boolean", default: true }) @Property({ columnType: "boolean" })
requires_shipping = true requires_shipping = true
@Property({ columnType: "boolean", default: true }) @Property({ columnType: "boolean" })
is_discountable = true is_discountable = true
@Property({ columnType: "boolean", default: false }) @Property({ columnType: "boolean" })
is_tax_inclusive = false is_tax_inclusive = false
@Property({ columnType: "numeric", nullable: true }) @Property({ columnType: "numeric", nullable: true })
+6 -3
View File
@@ -16,16 +16,20 @@ import ShippingMethodAdjustmentLine from "./shipping-method-adjustment-line"
import ShippingMethodTaxLine from "./shipping-method-tax-line" import ShippingMethodTaxLine from "./shipping-method-tax-line"
@Entity({ tableName: "cart_shipping_method" }) @Entity({ tableName: "cart_shipping_method" })
@Check<ShippingMethod>({ expression: (columns) => `${columns.amount} >= 0` })
export default class ShippingMethod { export default class ShippingMethod {
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id: string id: string
@Property({ columnType: "text" })
cart_id: string
@ManyToOne(() => Cart, { @ManyToOne(() => Cart, {
onDelete: "cascade", onDelete: "cascade",
index: "IDX_shipping_method_cart_id", index: "IDX_shipping_method_cart_id",
fieldName: "cart_id", nullable: true,
}) })
cart: Cart cart?: Cart | null
@Property({ columnType: "text" }) @Property({ columnType: "text" })
name: string name: string
@@ -34,7 +38,6 @@ export default class ShippingMethod {
description?: string | null description?: string | null
@Property({ columnType: "numeric", serializer: Number }) @Property({ columnType: "numeric", serializer: Number })
@Check({ expression: "amount >= 0" }) // TODO: Validate that numeric types work with the expression
amount: number amount: number
@Property({ columnType: "boolean" }) @Property({ columnType: "boolean" })
+6 -1
View File
@@ -8,4 +8,9 @@ export class AddressRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: CreateAddressDTO create: CreateAddressDTO
update: UpdateAddressDTO update: UpdateAddressDTO
} }
>(Address) {} >(Address) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
+1
View File
@@ -2,4 +2,5 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export * from "./address" export * from "./address"
export * from "./cart" export * from "./cart"
export * from "./line-item" export * from "./line-item"
export * from "./shipping-method"
+6 -1
View File
@@ -8,4 +8,9 @@ export class LineItemRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: CreateLineItemDTO create: CreateLineItemDTO
update: UpdateLineItemDTO update: UpdateLineItemDTO
} }
>(LineItem) {} >(LineItem) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -0,0 +1,16 @@
import { DALUtils } from "@medusajs/utils"
import { ShippingMethod } from "@models"
import { CreateShippingMethodDTO, UpdateShippingMethodDTO } from "@types"
export class ShippingMethodRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
ShippingMethod,
{
create: CreateShippingMethodDTO
update: UpdateShippingMethodDTO
}
>(ShippingMethod) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
+132 -1
View File
@@ -17,7 +17,7 @@ import {
isObject, isObject,
isString, isString,
} from "@medusajs/utils" } from "@medusajs/utils"
import { LineItem } from "@models" import { LineItem, ShippingMethod } from "@models"
import { UpdateLineItemDTO } from "@types" import { UpdateLineItemDTO } from "@types"
import { joinerConfig } from "../joiner-config" import { joinerConfig } from "../joiner-config"
import * as services from "../services" import * as services from "../services"
@@ -27,6 +27,7 @@ type InjectedDependencies = {
cartService: services.CartService cartService: services.CartService
addressService: services.AddressService addressService: services.AddressService
lineItemService: services.LineItemService lineItemService: services.LineItemService
shippingMethodService: services.ShippingMethodService
} }
export default class CartModuleService implements ICartModuleService { export default class CartModuleService implements ICartModuleService {
@@ -34,6 +35,7 @@ export default class CartModuleService implements ICartModuleService {
protected cartService_: services.CartService protected cartService_: services.CartService
protected addressService_: services.AddressService protected addressService_: services.AddressService
protected lineItemService_: services.LineItemService protected lineItemService_: services.LineItemService
protected shippingMethodService_: services.ShippingMethodService
constructor( constructor(
{ {
@@ -41,6 +43,7 @@ export default class CartModuleService implements ICartModuleService {
cartService, cartService,
addressService, addressService,
lineItemService, lineItemService,
shippingMethodService,
}: InjectedDependencies, }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
@@ -48,6 +51,7 @@ export default class CartModuleService implements ICartModuleService {
this.cartService_ = cartService this.cartService_ = cartService
this.addressService_ = addressService this.addressService_ = addressService
this.lineItemService_ = lineItemService this.lineItemService_ = lineItemService
this.shippingMethodService_ = shippingMethodService
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
@@ -252,6 +256,25 @@ export default class CartModuleService implements ICartModuleService {
) )
} }
@InjectManager("baseRepository_")
async listShippingMethods(
filters: CartTypes.FilterableShippingMethodProps = {},
config: FindConfig<CartTypes.CartShippingMethodDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<CartTypes.CartShippingMethodDTO[]> {
const methods = await this.shippingMethodService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<
CartTypes.CartShippingMethodDTO[]
>(methods, {
populate: true,
})
}
addLineItems( addLineItems(
data: CartTypes.CreateLineItemForCartDTO data: CartTypes.CreateLineItemForCartDTO
): Promise<CartTypes.CartLineItemDTO> ): Promise<CartTypes.CartLineItemDTO>
@@ -527,4 +550,112 @@ export default class CartModuleService implements ICartModuleService {
const addressIds = Array.isArray(ids) ? ids : [ids] const addressIds = Array.isArray(ids) ? ids : [ids]
await this.addressService_.delete(addressIds, sharedContext) await this.addressService_.delete(addressIds, sharedContext)
} }
async addShippingMethods(
data: CartTypes.CreateShippingMethodDTO
): Promise<CartTypes.CartShippingMethodDTO>
async addShippingMethods(
data: CartTypes.CreateShippingMethodDTO[]
): Promise<CartTypes.CartShippingMethodDTO[]>
async addShippingMethods(
cartId: string,
methods: CartTypes.CreateShippingMethodDTO[],
sharedContext?: Context
): Promise<CartTypes.CartShippingMethodDTO[]>
@InjectManager("baseRepository_")
async addShippingMethods(
cartIdOrData:
| string
| CartTypes.CreateShippingMethodDTO[]
| CartTypes.CreateShippingMethodDTO,
data?: CartTypes.CreateShippingMethodDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
CartTypes.CartShippingMethodDTO[] | CartTypes.CartShippingMethodDTO
> {
let methods: ShippingMethod[] = []
if (isString(cartIdOrData)) {
methods = await this.addShippingMethods_(
cartIdOrData,
data as CartTypes.CreateShippingMethodDTO[],
sharedContext
)
} else {
const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData]
methods = await this.addShippingMethodsBulk_(data, sharedContext)
}
return await this.baseRepository_.serialize<
CartTypes.CartShippingMethodDTO[]
>(methods, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
protected async addShippingMethods_(
cartId: string,
data: CartTypes.CreateShippingMethodDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ShippingMethod[]> {
const cart = await this.retrieve(cartId, { select: ["id"] }, sharedContext)
const methods = data.map((method) => {
return {
...method,
cart_id: cart.id,
}
})
return await this.addShippingMethodsBulk_(methods, sharedContext)
}
@InjectTransactionManager("baseRepository_")
protected async addShippingMethodsBulk_(
data: CartTypes.CreateShippingMethodDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ShippingMethod[]> {
return await this.shippingMethodService_.create(data, sharedContext)
}
async removeShippingMethods(
methodIds: string[],
sharedContext?: Context
): Promise<void>
async removeShippingMethods(
methodIds: string,
sharedContext?: Context
): Promise<void>
async removeShippingMethods(
selector: Partial<CartTypes.CartShippingMethodDTO>,
sharedContext?: Context
): Promise<void>
@InjectTransactionManager("baseRepository_")
async removeShippingMethods(
methodIdsOrSelector:
| string
| string[]
| Partial<CartTypes.CartShippingMethodDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
let toDelete: string[] = []
if (isObject(methodIdsOrSelector)) {
const methods = await this.listShippingMethods(
{
...(methodIdsOrSelector as Partial<CartTypes.CartShippingMethodDTO>),
},
{},
sharedContext
)
toDelete = methods.map((m) => m.id)
} else {
toDelete = Array.isArray(methodIdsOrSelector)
? methodIdsOrSelector
: [methodIdsOrSelector]
}
await this.shippingMethodService_.delete(toDelete, sharedContext)
}
} }
+1 -1
View File
@@ -16,7 +16,7 @@ export default class CartService<
update: UpdateCartDTO update: UpdateCartDTO
} }
>(Cart)<TEntity> { >(Cart)<TEntity> {
constructor({ cartRepository }: InjectedDependencies) { constructor(container: InjectedDependencies) {
// @ts-ignore // @ts-ignore
super(...arguments) super(...arguments)
} }
+1
View File
@@ -2,4 +2,5 @@ export { default as AddressService } from "./address"
export { default as CartService } from "./cart" export { default as CartService } from "./cart"
export { default as CartModuleService } from "./cart-module" export { default as CartModuleService } from "./cart-module"
export { default as LineItemService } from "./line-item" export { default as LineItemService } from "./line-item"
export { default as ShippingMethodService } from "./shipping-method"
+7 -2
View File
@@ -1,7 +1,7 @@
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils" import { ModulesSdkUtils } from "@medusajs/utils"
import { LineItem } from "@models" import { LineItem } from "@models"
import { CreateLineItemDTO, UpdateLineItemDTO } from "../types" import { CreateLineItemDTO, UpdateLineItemDTO } from "@types"
type InjectedDependencies = { type InjectedDependencies = {
lineItemRepository: DAL.RepositoryService lineItemRepository: DAL.RepositoryService
@@ -15,4 +15,9 @@ export default class LineItemService<
create: CreateLineItemDTO create: CreateLineItemDTO
update: UpdateLineItemDTO update: UpdateLineItemDTO
} }
>(LineItem)<TEntity> {} >(LineItem)<TEntity> {
constructor(container: InjectedDependencies) {
// @ts-ignore
super(...arguments)
}
}
@@ -0,0 +1,23 @@
import { DAL } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils"
import { ShippingMethod } from "@models"
import { CreateShippingMethodDTO, UpdateShippingMethodDTO } from "../types"
type InjectedDependencies = {
shippingMethodRepository: DAL.RepositoryService
}
export default class ShippingMethodService<
TEntity extends ShippingMethod = ShippingMethod
> extends ModulesSdkUtils.abstractServiceFactory<
InjectedDependencies,
{
create: CreateShippingMethodDTO
update: UpdateShippingMethodDTO
}
>(ShippingMethod)<TEntity> {
constructor(container: InjectedDependencies) {
// @ts-ignore
super(...arguments)
}
}
+1
View File
@@ -2,6 +2,7 @@ import { Logger } from "@medusajs/types"
export * from "./address" export * from "./address"
export * from "./cart" export * from "./cart"
export * from "./line-item" export * from "./line-item"
export * from "./shipping-method"
export type InitializeModuleInjectableDependencies = { export type InitializeModuleInjectableDependencies = {
logger?: Logger logger?: Logger
@@ -0,0 +1,13 @@
export interface CreateShippingMethodDTO {
name: string
cart_id: string
amount: number
data?: Record<string, unknown>
}
export interface UpdateShippingMethodDTO {
id: string
name?: string
amount?: number
data?: Record<string, unknown>
}
+14 -1
View File
@@ -168,6 +168,11 @@ export interface CartShippingMethodDTO {
*/ */
id: string id: string
/**
* The ID of the associated cart
*/
cart_id: string
/** /**
* The name of the shipping method * The name of the shipping method
*/ */
@@ -489,8 +494,16 @@ export interface FilterableLineItemProps
product_id?: string | string[] product_id?: string | string[]
} }
export interface FilterableShippingMethodProps
extends BaseFilterable<FilterableShippingMethodProps> {
id?: string | string[]
cart_id?: string | string[]
name?: string
shipping_option_id?: string | string[]
}
/** /**
* TODO: Remove this in favor of CartDTO, when module is released * TODO: Remove this in favor of CartDTO, when module is released
* @deprecated Use CartDTO instead * @deprecated Use CartDTO instead
*/ */
export type legacy_CartDTO = { export type legacy_CartDTO = {
+31 -8
View File
@@ -56,7 +56,7 @@ export interface UpdateCartDTO {
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
export interface CreateLineItemTaxLineDTO { export interface CreateTaxLineDTO {
description?: string description?: string
tax_rate_id?: string tax_rate_id?: string
code: string code: string
@@ -64,7 +64,7 @@ export interface CreateLineItemTaxLineDTO {
provider_id?: string provider_id?: string
} }
export interface CreateLineItemAdjustmentDTO { export interface CreateAdjustmentDTO {
code: string code: string
amount: number amount: number
description?: string description?: string
@@ -72,7 +72,7 @@ export interface CreateLineItemAdjustmentDTO {
provider_id?: string provider_id?: string
} }
export interface UpdateLineItemTaxLineDTO { export interface UpdateTaxLineDTO {
id: string id: string
description?: string description?: string
tax_rate_id?: string tax_rate_id?: string
@@ -81,7 +81,7 @@ export interface UpdateLineItemTaxLineDTO {
provider_id?: string provider_id?: string
} }
export interface UpdateLineItemAdjustmentDTO { export interface UpdateAdjustmentDTO {
id: string id: string
code?: string code?: string
amount?: number amount?: number
@@ -120,8 +120,8 @@ export interface CreateLineItemDTO {
compare_at_unit_price?: number compare_at_unit_price?: number
unit_price: number unit_price: number
tax_lines?: CreateLineItemTaxLineDTO[] tax_lines?: CreateTaxLineDTO[]
adjustments?: CreateLineItemAdjustmentDTO[] adjustments?: CreateAdjustmentDTO[]
} }
export interface CreateLineItemForCartDTO extends CreateLineItemDTO { export interface CreateLineItemForCartDTO extends CreateLineItemDTO {
@@ -144,6 +144,29 @@ export interface UpdateLineItemDTO
quantity?: number quantity?: number
unit_price?: number unit_price?: number
tax_lines?: UpdateLineItemTaxLineDTO[] | CreateLineItemTaxLineDTO[] tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[]
adjustments?: UpdateLineItemAdjustmentDTO[] | CreateLineItemAdjustmentDTO[] adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[]
}
export interface CreateShippingMethodDTO {
name: string
cart_id: string
amount: number
data?: Record<string, unknown>
tax_lines?: CreateTaxLineDTO[]
adjustments?: CreateAdjustmentDTO[]
}
export interface UpdateShippingMethodDTO {
id: string
name?: string
amount?: number
data?: Record<string, unknown>
tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[]
adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[]
} }
+34
View File
@@ -5,14 +5,17 @@ import {
CartAddressDTO, CartAddressDTO,
CartDTO, CartDTO,
CartLineItemDTO, CartLineItemDTO,
CartShippingMethodDTO,
FilterableAddressProps, FilterableAddressProps,
FilterableCartProps, FilterableCartProps,
FilterableShippingMethodProps,
} from "./common" } from "./common"
import { import {
CreateAddressDTO, CreateAddressDTO,
CreateCartDTO, CreateCartDTO,
CreateLineItemDTO, CreateLineItemDTO,
CreateLineItemForCartDTO, CreateLineItemForCartDTO,
CreateShippingMethodDTO,
UpdateAddressDTO, UpdateAddressDTO,
UpdateCartDTO, UpdateCartDTO,
UpdateLineItemDTO, UpdateLineItemDTO,
@@ -102,4 +105,35 @@ export interface ICartModuleService extends IModuleService {
selector: Partial<CartLineItemDTO>, selector: Partial<CartLineItemDTO>,
sharedContext?: Context sharedContext?: Context
): Promise<void> ): Promise<void>
listShippingMethods(
filters: FilterableShippingMethodProps,
config: FindConfig<CartShippingMethodDTO>,
sharedContext: Context
): Promise<CartShippingMethodDTO[]>
addShippingMethods(
data: CreateShippingMethodDTO
): Promise<CartShippingMethodDTO>
addShippingMethods(
data: CreateShippingMethodDTO[]
): Promise<CartShippingMethodDTO[]>
addShippingMethods(
cartId: string,
methods: CreateShippingMethodDTO[],
sharedContext?: Context
): Promise<CartShippingMethodDTO[]>
removeShippingMethods(
methodIds: string[],
sharedContext?: Context
): Promise<void>
removeShippingMethods(
methodIds: string,
sharedContext?: Context
): Promise<void>
removeShippingMethods(
selector: Partial<CartShippingMethodDTO>,
sharedContext?: Context
): Promise<void>
} }