diff --git a/.changeset/olive-ads-brake.md b/.changeset/olive-ads-brake.md new file mode 100644 index 0000000000000..a5fc56c03b432 --- /dev/null +++ b/.changeset/olive-ads-brake.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(cart): Shipping methods diff --git a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index acbdd99435945..980076263c808 100644 --- a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -1,4 +1,5 @@ import { ICartModuleService } from "@medusajs/types" +import { CheckConstraintViolationException } from "@mikro-orm/core" import { initialize } from "../../../../src/initialize" import { DB_URL, MikroOrmWrapper } from "../../../utils" @@ -466,12 +467,9 @@ describe("Cart Module Service", () => { expect(item.title).toBe("test") - const updatedItem = await service.updateLineItems( - item.id, - { - title: "test2", - } - ) + const updatedItem = await service.updateLineItems(item.id, { + title: "test2", + }) expect(updatedItem.title).toBe("test2") }) @@ -519,13 +517,13 @@ describe("Cart Module Service", () => { selector: { cart_id: createdCart.id }, data: { title: "changed-test", - } + }, }, { selector: { id: itemTwo!.id }, data: { title: "changed-other-test", - } + }, }, ]) @@ -603,4 +601,117 @@ describe("Cart Module Service", () => { 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) + }) + }) }) diff --git a/packages/cart/src/loaders/container.ts b/packages/cart/src/loaders/container.ts index ae8a1be50edc8..1affad85579f8 100644 --- a/packages/cart/src/loaders/container.ts +++ b/packages/cart/src/loaders/container.ts @@ -20,6 +20,9 @@ export default async ({ container.register({ cartService: asClass(defaultServices.CartService).singleton(), addressService: asClass(defaultServices.AddressService).singleton(), + shippingMethodService: asClass( + defaultServices.ShippingMethodService + ).singleton(), lineItemService: asClass(defaultServices.LineItemService).singleton(), }) @@ -38,7 +41,14 @@ function loadDefaultRepositories({ container }) { container.register({ baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), cartRepository: asClass(defaultRepositories.CartRepository).singleton(), - addressRepository: asClass(defaultRepositories.AddressRepository).singleton(), - lineItemRepository: asClass(defaultRepositories.LineItemRepository).singleton(), + addressRepository: asClass( + defaultRepositories.AddressRepository + ).singleton(), + lineItemRepository: asClass( + defaultRepositories.LineItemRepository + ).singleton(), + shippingMethodRepository: asClass( + defaultRepositories.ShippingMethodRepository + ).singleton(), }) } diff --git a/packages/cart/src/models/line-item.ts b/packages/cart/src/models/line-item.ts index da54c7046db2f..885de86489e4c 100644 --- a/packages/cart/src/models/line-item.ts +++ b/packages/cart/src/models/line-item.ts @@ -94,13 +94,13 @@ export default class LineItem { @Property({ columnType: "jsonb", nullable: true }) variant_option_values?: Record | null - @Property({ columnType: "boolean", default: true }) + @Property({ columnType: "boolean" }) requires_shipping = true - @Property({ columnType: "boolean", default: true }) + @Property({ columnType: "boolean" }) is_discountable = true - @Property({ columnType: "boolean", default: false }) + @Property({ columnType: "boolean" }) is_tax_inclusive = false @Property({ columnType: "numeric", nullable: true }) diff --git a/packages/cart/src/models/shipping-method.ts b/packages/cart/src/models/shipping-method.ts index c51478eefb3e1..9d8eab459c31e 100644 --- a/packages/cart/src/models/shipping-method.ts +++ b/packages/cart/src/models/shipping-method.ts @@ -16,16 +16,20 @@ import ShippingMethodAdjustmentLine from "./shipping-method-adjustment-line" import ShippingMethodTaxLine from "./shipping-method-tax-line" @Entity({ tableName: "cart_shipping_method" }) +@Check({ expression: (columns) => `${columns.amount} >= 0` }) export default class ShippingMethod { @PrimaryKey({ columnType: "text" }) id: string + @Property({ columnType: "text" }) + cart_id: string + @ManyToOne(() => Cart, { onDelete: "cascade", index: "IDX_shipping_method_cart_id", - fieldName: "cart_id", + nullable: true, }) - cart: Cart + cart?: Cart | null @Property({ columnType: "text" }) name: string @@ -34,7 +38,6 @@ export default class ShippingMethod { description?: string | null @Property({ columnType: "numeric", serializer: Number }) - @Check({ expression: "amount >= 0" }) // TODO: Validate that numeric types work with the expression amount: number @Property({ columnType: "boolean" }) diff --git a/packages/cart/src/repositories/address.ts b/packages/cart/src/repositories/address.ts index a480877948237..2fb052cf7fde7 100644 --- a/packages/cart/src/repositories/address.ts +++ b/packages/cart/src/repositories/address.ts @@ -8,4 +8,9 @@ export class AddressRepository extends DALUtils.mikroOrmBaseRepositoryFactory< create: CreateAddressDTO update: UpdateAddressDTO } ->(Address) {} +>(Address) { + constructor(...args: any[]) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/repositories/index.ts b/packages/cart/src/repositories/index.ts index fabd202b2db03..d1bf0c1118c4f 100644 --- a/packages/cart/src/repositories/index.ts +++ b/packages/cart/src/repositories/index.ts @@ -2,4 +2,5 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" export * from "./address" export * from "./cart" export * from "./line-item" +export * from "./shipping-method" diff --git a/packages/cart/src/repositories/line-item.ts b/packages/cart/src/repositories/line-item.ts index 6741e7ee89927..8ac5e3c8636d9 100644 --- a/packages/cart/src/repositories/line-item.ts +++ b/packages/cart/src/repositories/line-item.ts @@ -8,4 +8,9 @@ export class LineItemRepository extends DALUtils.mikroOrmBaseRepositoryFactory< create: CreateLineItemDTO update: UpdateLineItemDTO } ->(LineItem) {} +>(LineItem) { + constructor(...args: any[]) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/repositories/shipping-method.ts b/packages/cart/src/repositories/shipping-method.ts new file mode 100644 index 0000000000000..2b228ba5a4697 --- /dev/null +++ b/packages/cart/src/repositories/shipping-method.ts @@ -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) + } +} diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index 5a20a5e9eaafa..b60686ddd6a29 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -17,7 +17,7 @@ import { isObject, isString, } from "@medusajs/utils" -import { LineItem } from "@models" +import { LineItem, ShippingMethod } from "@models" import { UpdateLineItemDTO } from "@types" import { joinerConfig } from "../joiner-config" import * as services from "../services" @@ -27,6 +27,7 @@ type InjectedDependencies = { cartService: services.CartService addressService: services.AddressService lineItemService: services.LineItemService + shippingMethodService: services.ShippingMethodService } export default class CartModuleService implements ICartModuleService { @@ -34,6 +35,7 @@ export default class CartModuleService implements ICartModuleService { protected cartService_: services.CartService protected addressService_: services.AddressService protected lineItemService_: services.LineItemService + protected shippingMethodService_: services.ShippingMethodService constructor( { @@ -41,6 +43,7 @@ export default class CartModuleService implements ICartModuleService { cartService, addressService, lineItemService, + shippingMethodService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -48,6 +51,7 @@ export default class CartModuleService implements ICartModuleService { this.cartService_ = cartService this.addressService_ = addressService this.lineItemService_ = lineItemService + this.shippingMethodService_ = shippingMethodService } __joinerConfig(): ModuleJoinerConfig { @@ -252,6 +256,25 @@ export default class CartModuleService implements ICartModuleService { ) } + @InjectManager("baseRepository_") + async listShippingMethods( + filters: CartTypes.FilterableShippingMethodProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const methods = await this.shippingMethodService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize< + CartTypes.CartShippingMethodDTO[] + >(methods, { + populate: true, + }) + } + addLineItems( data: CartTypes.CreateLineItemForCartDTO ): Promise @@ -527,4 +550,112 @@ export default class CartModuleService implements ICartModuleService { const addressIds = Array.isArray(ids) ? ids : [ids] await this.addressService_.delete(addressIds, sharedContext) } + + async addShippingMethods( + data: CartTypes.CreateShippingMethodDTO + ): Promise + async addShippingMethods( + data: CartTypes.CreateShippingMethodDTO[] + ): Promise + async addShippingMethods( + cartId: string, + methods: CartTypes.CreateShippingMethodDTO[], + sharedContext?: Context + ): Promise + + @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 { + 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 { + return await this.shippingMethodService_.create(data, sharedContext) + } + + async removeShippingMethods( + methodIds: string[], + sharedContext?: Context + ): Promise + async removeShippingMethods( + methodIds: string, + sharedContext?: Context + ): Promise + async removeShippingMethods( + selector: Partial, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async removeShippingMethods( + methodIdsOrSelector: + | string + | string[] + | Partial, + @MedusaContext() sharedContext: Context = {} + ): Promise { + let toDelete: string[] = [] + if (isObject(methodIdsOrSelector)) { + const methods = await this.listShippingMethods( + { + ...(methodIdsOrSelector as Partial), + }, + {}, + sharedContext + ) + + toDelete = methods.map((m) => m.id) + } else { + toDelete = Array.isArray(methodIdsOrSelector) + ? methodIdsOrSelector + : [methodIdsOrSelector] + } + await this.shippingMethodService_.delete(toDelete, sharedContext) + } } diff --git a/packages/cart/src/services/cart.ts b/packages/cart/src/services/cart.ts index e206f557c3637..a9c594555b1ca 100644 --- a/packages/cart/src/services/cart.ts +++ b/packages/cart/src/services/cart.ts @@ -16,7 +16,7 @@ export default class CartService< update: UpdateCartDTO } >(Cart) { - constructor({ cartRepository }: InjectedDependencies) { + constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } diff --git a/packages/cart/src/services/index.ts b/packages/cart/src/services/index.ts index a72d00a3361ea..b3c18ffa65e7d 100644 --- a/packages/cart/src/services/index.ts +++ b/packages/cart/src/services/index.ts @@ -2,4 +2,5 @@ export { default as AddressService } from "./address" export { default as CartService } from "./cart" export { default as CartModuleService } from "./cart-module" export { default as LineItemService } from "./line-item" +export { default as ShippingMethodService } from "./shipping-method" diff --git a/packages/cart/src/services/line-item.ts b/packages/cart/src/services/line-item.ts index 0794665e737ed..ec736f4010249 100644 --- a/packages/cart/src/services/line-item.ts +++ b/packages/cart/src/services/line-item.ts @@ -1,7 +1,7 @@ import { DAL } from "@medusajs/types" import { ModulesSdkUtils } from "@medusajs/utils" import { LineItem } from "@models" -import { CreateLineItemDTO, UpdateLineItemDTO } from "../types" +import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" type InjectedDependencies = { lineItemRepository: DAL.RepositoryService @@ -15,4 +15,9 @@ export default class LineItemService< create: CreateLineItemDTO update: UpdateLineItemDTO } ->(LineItem) {} +>(LineItem) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/services/shipping-method.ts b/packages/cart/src/services/shipping-method.ts new file mode 100644 index 0000000000000..f3cf6671442ee --- /dev/null +++ b/packages/cart/src/services/shipping-method.ts @@ -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) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/types/index.ts b/packages/cart/src/types/index.ts index e9e853846c725..243d7d3c8d49f 100644 --- a/packages/cart/src/types/index.ts +++ b/packages/cart/src/types/index.ts @@ -2,6 +2,7 @@ import { Logger } from "@medusajs/types" export * from "./address" export * from "./cart" export * from "./line-item" +export * from "./shipping-method" export type InitializeModuleInjectableDependencies = { logger?: Logger diff --git a/packages/cart/src/types/shipping-method.ts b/packages/cart/src/types/shipping-method.ts new file mode 100644 index 0000000000000..6e70a4056bc0c --- /dev/null +++ b/packages/cart/src/types/shipping-method.ts @@ -0,0 +1,13 @@ +export interface CreateShippingMethodDTO { + name: string + cart_id: string + amount: number + data?: Record +} + +export interface UpdateShippingMethodDTO { + id: string + name?: string + amount?: number + data?: Record +} diff --git a/packages/types/src/cart/common.ts b/packages/types/src/cart/common.ts index 6c345d59454f3..508bfc8dc7733 100644 --- a/packages/types/src/cart/common.ts +++ b/packages/types/src/cart/common.ts @@ -168,6 +168,11 @@ export interface CartShippingMethodDTO { */ id: string + /** + * The ID of the associated cart + */ + cart_id: string + /** * The name of the shipping method */ @@ -489,8 +494,16 @@ export interface FilterableLineItemProps product_id?: string | string[] } +export interface FilterableShippingMethodProps + extends BaseFilterable { + 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 */ export type legacy_CartDTO = { diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index f128d819c911c..294180cd89c52 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -56,7 +56,7 @@ export interface UpdateCartDTO { metadata?: Record } -export interface CreateLineItemTaxLineDTO { +export interface CreateTaxLineDTO { description?: string tax_rate_id?: string code: string @@ -64,7 +64,7 @@ export interface CreateLineItemTaxLineDTO { provider_id?: string } -export interface CreateLineItemAdjustmentDTO { +export interface CreateAdjustmentDTO { code: string amount: number description?: string @@ -72,7 +72,7 @@ export interface CreateLineItemAdjustmentDTO { provider_id?: string } -export interface UpdateLineItemTaxLineDTO { +export interface UpdateTaxLineDTO { id: string description?: string tax_rate_id?: string @@ -81,7 +81,7 @@ export interface UpdateLineItemTaxLineDTO { provider_id?: string } -export interface UpdateLineItemAdjustmentDTO { +export interface UpdateAdjustmentDTO { id: string code?: string amount?: number @@ -120,8 +120,8 @@ export interface CreateLineItemDTO { compare_at_unit_price?: number unit_price: number - tax_lines?: CreateLineItemTaxLineDTO[] - adjustments?: CreateLineItemAdjustmentDTO[] + tax_lines?: CreateTaxLineDTO[] + adjustments?: CreateAdjustmentDTO[] } export interface CreateLineItemForCartDTO extends CreateLineItemDTO { @@ -144,6 +144,29 @@ export interface UpdateLineItemDTO quantity?: number unit_price?: number - tax_lines?: UpdateLineItemTaxLineDTO[] | CreateLineItemTaxLineDTO[] - adjustments?: UpdateLineItemAdjustmentDTO[] | CreateLineItemAdjustmentDTO[] + tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[] + adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[] +} + +export interface CreateShippingMethodDTO { + name: string + + cart_id: string + + amount: number + data?: Record + + tax_lines?: CreateTaxLineDTO[] + adjustments?: CreateAdjustmentDTO[] +} + +export interface UpdateShippingMethodDTO { + id: string + name?: string + + amount?: number + data?: Record + + tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[] + adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[] } diff --git a/packages/types/src/cart/service.ts b/packages/types/src/cart/service.ts index 9f7f413acc3ff..14e6222998d17 100644 --- a/packages/types/src/cart/service.ts +++ b/packages/types/src/cart/service.ts @@ -5,14 +5,17 @@ import { CartAddressDTO, CartDTO, CartLineItemDTO, + CartShippingMethodDTO, FilterableAddressProps, FilterableCartProps, + FilterableShippingMethodProps, } from "./common" import { CreateAddressDTO, CreateCartDTO, CreateLineItemDTO, CreateLineItemForCartDTO, + CreateShippingMethodDTO, UpdateAddressDTO, UpdateCartDTO, UpdateLineItemDTO, @@ -102,4 +105,35 @@ export interface ICartModuleService extends IModuleService { selector: Partial, sharedContext?: Context ): Promise + + listShippingMethods( + filters: FilterableShippingMethodProps, + config: FindConfig, + sharedContext: Context + ): Promise + + addShippingMethods( + data: CreateShippingMethodDTO + ): Promise + addShippingMethods( + data: CreateShippingMethodDTO[] + ): Promise + addShippingMethods( + cartId: string, + methods: CreateShippingMethodDTO[], + sharedContext?: Context + ): Promise + + removeShippingMethods( + methodIds: string[], + sharedContext?: Context + ): Promise + removeShippingMethods( + methodIds: string, + sharedContext?: Context + ): Promise + removeShippingMethods( + selector: Partial, + sharedContext?: Context + ): Promise }