From 06b33a9b4525b77b1b14b35b973209700945654e Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:55:47 +0100 Subject: [PATCH] feat(cart): Line item adjustments (#6112) --- .changeset/sour-geckos-wash.md | 6 + .../services/cart-module/index.spec.ts | 511 ++++++++++++++++++ packages/cart/jest.config.js | 2 + packages/cart/src/index.ts | 5 +- packages/cart/src/models/adjustment-line.ts | 12 +- packages/cart/src/models/index.ts | 2 +- ...stment-line.ts => line-item-adjustment.ts} | 19 +- packages/cart/src/models/line-item.ts | 8 +- packages/cart/src/module-definition.ts | 2 +- packages/cart/src/repositories/index.ts | 3 +- packages/cart/src/services/cart-module.ts | 229 +++++++- packages/cart/src/services/index.ts | 1 + .../cart/src/services/line-item-adjustment.ts | 26 + packages/cart/src/types/cart.ts | 7 + packages/cart/src/types/index.ts | 1 + .../cart/src/types/line-item-adjustment.ts | 13 + packages/cart/src/types/repositories.ts | 16 +- packages/types/src/cart/common.ts | 24 +- packages/types/src/cart/mutations.ts | 65 ++- packages/types/src/cart/service.ts | 87 ++- .../modules-sdk/abstract-service-factory.ts | 4 +- 21 files changed, 973 insertions(+), 70 deletions(-) create mode 100644 .changeset/sour-geckos-wash.md rename packages/cart/src/models/{line-item-adjustment-line.ts => line-item-adjustment.ts} (57%) create mode 100644 packages/cart/src/services/line-item-adjustment.ts create mode 100644 packages/cart/src/types/line-item-adjustment.ts diff --git a/.changeset/sour-geckos-wash.md b/.changeset/sour-geckos-wash.md new file mode 100644 index 0000000000000..7e907ab0d1e94 --- /dev/null +++ b/.changeset/sour-geckos-wash.md @@ -0,0 +1,6 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(cart): Line item adjustments 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 d444b634ef714..0e467ce23ea6c 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 @@ -799,4 +799,515 @@ describe("Cart Module Service", () => { expect(cart.shipping_methods?.length).toBe(0) }) }) + + describe("setLineItemAdjustments", () => { + it("should set line item adjustments for a cart", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const [itemTwo] = await service.addLineItems(createdCart.id, [ + { + quantity: 2, + unit_price: 200, + title: "test-2", + }, + ]) + + const adjustments = await service.setLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + { + item_id: itemTwo.id, + amount: 200, + code: "FREE-2", + }, + ]) + + expect(adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + expect.objectContaining({ + item_id: itemTwo.id, + amount: 200, + code: "FREE-2", + }), + ]) + ) + }) + + it("should replace line item adjustments for a cart", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const adjustments = await service.setLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + ]) + + expect(adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + ]) + ) + + await service.setLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 50, + code: "50%", + }, + ]) + + const cart = await service.retrieve(createdCart.id, { + relations: ["items.adjustments"], + }) + + expect(cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 50, + code: "50%", + }), + ]), + }), + ]) + ) + + expect(cart.items?.length).toBe(1) + expect(cart.items?.[0].adjustments?.length).toBe(1) + }) + + it("should remove all line item adjustments for a cart", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const adjustments = await service.setLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + ]) + + expect(adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + ]) + ) + + await service.setLineItemAdjustments(createdCart.id, []) + + const cart = await service.retrieve(createdCart.id, { + relations: ["items.adjustments"], + }) + + expect(cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + adjustments: [], + }), + ]) + ) + + expect(cart.items?.length).toBe(1) + expect(cart.items?.[0].adjustments?.length).toBe(0) + }) + + it("should update line item adjustments for a cart", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const adjustments = await service.setLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + ]) + + expect(adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + ]) + ) + + await service.setLineItemAdjustments(createdCart.id, [ + { + id: adjustments[0].id, + amount: 50, + code: "50%", + }, + ]) + + const cart = await service.retrieve(createdCart.id, { + relations: ["items.adjustments"], + }) + + expect(cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + adjustments: [ + expect.objectContaining({ + id: adjustments[0].id, + item_id: itemOne.id, + amount: 50, + code: "50%", + }), + ], + }), + ]) + ) + + expect(cart.items?.length).toBe(1) + expect(cart.items?.[0].adjustments?.length).toBe(1) + }) + }) + + describe("addLineItemAdjustments", () => { + it("should add line item adjustments for items in a cart", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const adjustments = await service.addLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + ]) + + expect(adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + ]) + ) + }) + + it("should add multiple line item adjustments for multiple line items", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + const [itemTwo] = await service.addLineItems(createdCart.id, [ + { + quantity: 2, + unit_price: 200, + title: "test-2", + }, + ]) + + const adjustments = await service.addLineItemAdjustments(createdCart.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + { + item_id: itemTwo.id, + amount: 150, + code: "CODE-2", + }, + ]) + + expect(adjustments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + expect.objectContaining({ + item_id: itemTwo.id, + amount: 150, + code: "CODE-2", + }), + ]) + ) + }) + + it("should add line item adjustments for line items on multiple carts", async () => { + const [cartOne] = await service.create([ + { + currency_code: "eur", + }, + ]) + const [cartTwo] = await service.create([ + { + currency_code: "usd", + }, + ]) + + const [itemOne] = await service.addLineItems(cartOne.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + const [itemTwo] = await service.addLineItems(cartTwo.id, [ + { + quantity: 2, + unit_price: 200, + title: "test-2", + }, + ]) + + await service.addLineItemAdjustments([ + // item from cart one + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + // item from cart two + { + item_id: itemTwo.id, + amount: 150, + code: "CODE-2", + }, + ]) + + const cartOneItems = await service.listLineItems( + { cart_id: cartOne.id }, + { relations: ["adjustments"] } + ) + const cartTwoItems = await service.listLineItems( + { cart_id: cartTwo.id }, + { relations: ["adjustments"] } + ) + + expect(cartOneItems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + adjustments: expect.arrayContaining([ + expect.objectContaining({ + item_id: itemOne.id, + amount: 100, + code: "FREE", + }), + ]), + }), + ]) + ) + expect(cartTwoItems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + adjustments: expect.arrayContaining([ + expect.objectContaining({ + item_id: itemTwo.id, + amount: 150, + code: "CODE-2", + }), + ]), + }), + ]) + ) + }) + + it("should throw if line item is not associated with cart", async () => { + const [cartOne] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [cartTwo] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addLineItems(cartOne.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const error = await service + .addLineItemAdjustments(cartTwo.id, [ + { + item_id: itemOne.id, + amount: 100, + code: "FREE", + }, + ]) + .catch((e) => e) + + expect(error.message).toBe( + `Line item with id ${itemOne.id} does not exist on cart with id ${cartTwo.id}` + ) + }) + }) + + describe("removeLineItemAdjustments", () => { + it("should remove a line item succesfully", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [item] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const [adjustment] = await service.addLineItemAdjustments( + createdCart.id, + [ + { + item_id: item.id, + amount: 50, + }, + ] + ) + + expect(adjustment.item_id).toBe(item.id) + + await service.removeLineItemAdjustments(adjustment.id) + + const adjustments = await service.listLineItemAdjustments({ + item_id: item.id, + }) + + expect(adjustments?.length).toBe(0) + }) + + it("should remove a line item succesfully with selector", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [item] = await service.addLineItems(createdCart.id, [ + { + quantity: 1, + unit_price: 100, + title: "test", + }, + ]) + + const [adjustment] = await service.addLineItemAdjustments( + createdCart.id, + [ + { + item_id: item.id, + amount: 50, + }, + ] + ) + + expect(adjustment.item_id).toBe(item.id) + + await service.removeLineItemAdjustments({ item_id: item.id }) + + const adjustments = await service.listLineItemAdjustments({ + item_id: item.id, + }) + + expect(adjustments?.length).toBe(0) + }) + }) }) diff --git a/packages/cart/jest.config.js b/packages/cart/jest.config.js index 860ba90a49c5e..7117eaa088cd7 100644 --- a/packages/cart/jest.config.js +++ b/packages/cart/jest.config.js @@ -3,6 +3,8 @@ module.exports = { "^@models": "/src/models", "^@services": "/src/services", "^@repositories": "/src/repositories", + "^@types": "/src/types", + "^@utils": "/src/utils", }, transform: { "^.+\\.[jt]s?$": [ diff --git a/packages/cart/src/index.ts b/packages/cart/src/index.ts index e025c714a46f9..13081ed6847db 100644 --- a/packages/cart/src/index.ts +++ b/packages/cart/src/index.ts @@ -1,7 +1,7 @@ -import { moduleDefinition } from "./module-definition" import { Modules } from "@medusajs/modules-sdk" -import * as Models from "@models" import { ModulesSdkUtils } from "@medusajs/utils" +import * as Models from "@models" +import { moduleDefinition } from "./module-definition" export default moduleDefinition @@ -20,3 +20,4 @@ export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript( export * from "./initialize" export * from "./loaders" + diff --git a/packages/cart/src/models/adjustment-line.ts b/packages/cart/src/models/adjustment-line.ts index 4b714542a7237..e7c0961ac1ac5 100644 --- a/packages/cart/src/models/adjustment-line.ts +++ b/packages/cart/src/models/adjustment-line.ts @@ -14,19 +14,19 @@ export default abstract class AdjustmentLine { id: string @Property({ columnType: "text", nullable: true }) - description?: string | null + description: string | null = null @Property({ columnType: "text", nullable: true }) - promotion_id?: string | null + promotion_id: string | null = null - @Property({ columnType: "text" }) - code: string + @Property({ columnType: "text", nullable: true }) + code: string | null = null - @Property({ columnType: "numeric" }) + @Property({ columnType: "numeric", serializer: Number }) amount: number @Property({ columnType: "text", nullable: true }) - provider_id?: string | null + provider_id: string | null = null @Property({ onCreate: () => new Date(), diff --git a/packages/cart/src/models/index.ts b/packages/cart/src/models/index.ts index 9451e322dc3da..33f78f0616d42 100644 --- a/packages/cart/src/models/index.ts +++ b/packages/cart/src/models/index.ts @@ -1,7 +1,7 @@ export { default as Address } from "./address" export { default as Cart } from "./cart" export { default as LineItem } from "./line-item" -export { default as LineItemAdjustmentLine } from "./line-item-adjustment-line" +export { default as LineItemAdjustment } from "./line-item-adjustment" export { default as LineItemTaxLine } from "./line-item-tax-line" export { default as ShippingMethod } from "./shipping-method" export { default as ShippingMethodAdjustmentLine } from "./shipping-method-adjustment-line" diff --git a/packages/cart/src/models/line-item-adjustment-line.ts b/packages/cart/src/models/line-item-adjustment.ts similarity index 57% rename from packages/cart/src/models/line-item-adjustment-line.ts rename to packages/cart/src/models/line-item-adjustment.ts index b26250fdd47a6..7e355ca463e04 100644 --- a/packages/cart/src/models/line-item-adjustment-line.ts +++ b/packages/cart/src/models/line-item-adjustment.ts @@ -1,20 +1,29 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Check, Entity, ManyToOne, - OnInit + OnInit, + Property, } from "@mikro-orm/core" import AdjustmentLine from "./adjustment-line" import LineItem from "./line-item" @Entity({ tableName: "cart_line_item_adjustment_line" }) -export default class LineItemAdjustmentLine extends AdjustmentLine { +@Check({ + expression: (columns) => `${columns.amount} >= 0`, +}) +export default class LineItemAdjustment extends AdjustmentLine { @ManyToOne(() => LineItem, { - joinColumn: "line_item", - fieldName: "line_item_id", + onDelete: "cascade", + nullable: true, + index: "IDX_adjustment_line_item_id", }) - line_item: LineItem + item?: LineItem | null + + @Property({ columnType: "text" }) + item_id: string @BeforeCreate() onCreate() { diff --git a/packages/cart/src/models/line-item.ts b/packages/cart/src/models/line-item.ts index 885de86489e4c..4ed2f897196be 100644 --- a/packages/cart/src/models/line-item.ts +++ b/packages/cart/src/models/line-item.ts @@ -14,7 +14,7 @@ import { Property, } from "@mikro-orm/core" import Cart from "./cart" -import LineItemAdjustmentLine from "./line-item-adjustment-line" +import LineItemAdjustment from "./line-item-adjustment" import LineItemTaxLine from "./line-item-tax-line" type OptionalLineItemProps = @@ -116,13 +116,13 @@ export default class LineItem { tax_lines = new Collection(this) @OneToMany( - () => LineItemAdjustmentLine, - (adjustment) => adjustment.line_item, + () => LineItemAdjustment, + (adjustment) => adjustment.item, { cascade: [Cascade.REMOVE], } ) - adjustments = new Collection(this) + adjustments = new Collection(this) /** COMPUTED PROPERTIES - START */ diff --git a/packages/cart/src/module-definition.ts b/packages/cart/src/module-definition.ts index c4baffa5f515e..133814940b6ff 100644 --- a/packages/cart/src/module-definition.ts +++ b/packages/cart/src/module-definition.ts @@ -1,7 +1,7 @@ import { ModuleExports } from "@medusajs/types" -import { CartModuleService } from "@services" import loadConnection from "./loaders/connection" import loadContainer from "./loaders/container" +import { CartModuleService } from "./services" const service = CartModuleService const loaders = [loadContainer, loadConnection] as any diff --git a/packages/cart/src/repositories/index.ts b/packages/cart/src/repositories/index.ts index 147c9cc259fa4..d905f87a659aa 100644 --- a/packages/cart/src/repositories/index.ts +++ b/packages/cart/src/repositories/index.ts @@ -1 +1,2 @@ -export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" +export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"; + diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index 8c6a84d99b2e0..f9d479284cf1f 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -1,23 +1,21 @@ import { + CartTypes, Context, DAL, - FilterableCartProps, FindConfig, ICartModuleService, InternalModuleDeclaration, ModuleJoinerConfig, } from "@medusajs/types" - -import { CartTypes } from "@medusajs/types" - import { InjectManager, InjectTransactionManager, MedusaContext, + MedusaError, isObject, isString, } from "@medusajs/utils" -import { Cart, LineItem, ShippingMethod } from "@models" +import { Cart, LineItem, LineItemAdjustment, ShippingMethod } from "@models" import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" import { joinerConfig } from "../joiner-config" import * as services from "../services" @@ -28,6 +26,7 @@ type InjectedDependencies = { addressService: services.AddressService lineItemService: services.LineItemService shippingMethodService: services.ShippingMethodService + lineItemAdjustmentService: services.LineItemAdjustmentService } export default class CartModuleService implements ICartModuleService { @@ -36,6 +35,7 @@ export default class CartModuleService implements ICartModuleService { protected addressService_: services.AddressService protected lineItemService_: services.LineItemService protected shippingMethodService_: services.ShippingMethodService + protected lineItemAdjustmentService_: services.LineItemAdjustmentService constructor( { @@ -44,6 +44,7 @@ export default class CartModuleService implements ICartModuleService { addressService, lineItemService, shippingMethodService, + lineItemAdjustmentService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -52,6 +53,7 @@ export default class CartModuleService implements ICartModuleService { this.addressService_ = addressService this.lineItemService_ = lineItemService this.shippingMethodService_ = shippingMethodService + this.lineItemAdjustmentService_ = lineItemAdjustmentService } __joinerConfig(): ModuleJoinerConfig { @@ -86,7 +88,7 @@ export default class CartModuleService implements ICartModuleService { @InjectManager("baseRepository_") async listAndCount( - filters: FilterableCartProps = {}, + filters: CartTypes.FilterableCartProps = {}, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[CartTypes.CartDTO[], number]> { @@ -244,14 +246,14 @@ export default class CartModuleService implements ICartModuleService { itemId: string, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { const item = await this.lineItemService_.retrieve( itemId, config, sharedContext ) - return await this.baseRepository_.serialize( + return await this.baseRepository_.serialize( item, { populate: true, @@ -300,7 +302,7 @@ export default class CartModuleService implements ICartModuleService { addLineItems( data: CartTypes.CreateLineItemForCartDTO - ): Promise + ): Promise addLineItems( data: CartTypes.CreateLineItemForCartDTO[] ): Promise @@ -318,7 +320,7 @@ export default class CartModuleService implements ICartModuleService { | CartTypes.CreateLineItemForCartDTO, data?: CartTypes.CreateLineItemDTO[] | CartTypes.CreateLineItemDTO, @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise { let items: LineItem[] = [] if (isString(cartIdOrData)) { items = await this.addLineItems_( @@ -592,7 +594,9 @@ export default class CartModuleService implements ICartModuleService { | string | CartTypes.CreateShippingMethodDTO[] | CartTypes.CreateShippingMethodDTO, - data?: CartTypes.CreateShippingMethodDTO[], + data?: + | CartTypes.CreateShippingMethodDTO[] + | CartTypes.CreateShippingMethodForSingleCartDTO[], @MedusaContext() sharedContext: Context = {} ): Promise< CartTypes.CartShippingMethodDTO[] | CartTypes.CartShippingMethodDTO @@ -601,12 +605,15 @@ export default class CartModuleService implements ICartModuleService { if (isString(cartIdOrData)) { methods = await this.addShippingMethods_( cartIdOrData, - data as CartTypes.CreateShippingMethodDTO[], + data as CartTypes.CreateShippingMethodForSingleCartDTO[], sharedContext ) } else { const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData] - methods = await this.addShippingMethodsBulk_(data, sharedContext) + methods = await this.addShippingMethodsBulk_( + data as CartTypes.CreateShippingMethodDTO[], + sharedContext + ) } return await this.baseRepository_.serialize< @@ -619,7 +626,7 @@ export default class CartModuleService implements ICartModuleService { @InjectTransactionManager("baseRepository_") protected async addShippingMethods_( cartId: string, - data: CartTypes.CreateShippingMethodDTO[], + data: CartTypes.CreateShippingMethodForSingleCartDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { const cart = await this.retrieve(cartId, { select: ["id"] }, sharedContext) @@ -681,4 +688,198 @@ export default class CartModuleService implements ICartModuleService { } await this.shippingMethodService_.delete(toDelete, sharedContext) } + + @InjectManager("baseRepository_") + async listLineItemAdjustments( + filters: CartTypes.FilterableLineItemAdjustmentProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const adjustments = await this.lineItemAdjustmentService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize< + CartTypes.LineItemAdjustmentDTO[] + >(adjustments, { + populate: true, + }) + } + + async addLineItemAdjustments( + adjustments: CartTypes.CreateLineItemAdjustmentDTO[] + ): Promise + async addLineItemAdjustments( + adjustment: CartTypes.CreateLineItemAdjustmentDTO + ): Promise + async addLineItemAdjustments( + cartId: string, + adjustments: CartTypes.CreateLineItemAdjustmentDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async addLineItemAdjustments( + cartIdOrData: + | string + | CartTypes.CreateLineItemAdjustmentDTO[] + | CartTypes.CreateLineItemAdjustmentDTO, + adjustments?: CartTypes.CreateLineItemAdjustmentDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + let addedAdjustments: LineItemAdjustment[] = [] + if (isString(cartIdOrData)) { + const cart = await this.retrieve( + cartIdOrData, + { select: ["id"], relations: ["items"] }, + sharedContext + ) + + const lineIds = cart.items?.map((item) => item.id) + + for (const adj of adjustments || []) { + if (!lineIds?.includes(adj.item_id)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Line item with id ${adj.item_id} does not exist on cart with id ${cartIdOrData}` + ) + } + } + + addedAdjustments = await this.lineItemAdjustmentService_.create( + adjustments as CartTypes.CreateLineItemAdjustmentDTO[], + sharedContext + ) + } else { + const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData] + + addedAdjustments = await this.lineItemAdjustmentService_.create( + data as CartTypes.CreateLineItemAdjustmentDTO[], + sharedContext + ) + } + + return await this.baseRepository_.serialize< + CartTypes.LineItemAdjustmentDTO[] + >(addedAdjustments, { + populate: true, + }) + } + + @InjectTransactionManager("baseRepository_") + async setLineItemAdjustments( + cartId: string, + adjustments: ( + | CartTypes.CreateLineItemAdjustmentDTO + | CartTypes.UpdateLineItemAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const cart = await this.retrieve( + cartId, + { select: ["id"], relations: ["items.adjustments"] }, + sharedContext + ) + + const lineIds = cart.items?.map((item) => item.id) + + const existingAdjustments = await this.listLineItemAdjustments( + { item_id: lineIds }, + { select: ["id"] }, + sharedContext + ) + + let toUpdate: CartTypes.UpdateLineItemAdjustmentDTO[] = [] + let toCreate: CartTypes.CreateLineItemAdjustmentDTO[] = [] + for (const adj of adjustments) { + if ("id" in adj) { + toUpdate.push(adj as CartTypes.UpdateLineItemAdjustmentDTO) + } else { + toCreate.push(adj as CartTypes.CreateLineItemAdjustmentDTO) + } + } + + const adjustmentsSet = new Set(toUpdate.map((a) => a.id)) + + const toDelete: CartTypes.LineItemAdjustmentDTO[] = [] + + // From the existing adjustments, find the ones that are not passed in adjustments + existingAdjustments.forEach((adj: CartTypes.LineItemAdjustmentDTO) => { + if (!adjustmentsSet.has(adj.id)) { + toDelete.push(adj) + } + }) + + await this.lineItemAdjustmentService_.delete( + toDelete.map((adj) => adj!.id), + sharedContext + ) + + let result: LineItemAdjustment[] = [] + + // TODO: Replace the following two calls with a single bulk upsert call + if (toUpdate?.length) { + const updated = await this.lineItemAdjustmentService_.update( + toUpdate, + sharedContext + ) + result.push(...updated) + } + + if (toCreate?.length) { + const created = await this.lineItemAdjustmentService_.create( + toCreate, + sharedContext + ) + result.push(...created) + } + + return await this.baseRepository_.serialize< + CartTypes.LineItemAdjustmentDTO[] + >(result, { + populate: true, + }) + } + + async removeLineItemAdjustments( + adjustmentIds: string[], + sharedContext?: Context + ): Promise + async removeLineItemAdjustments( + adjustmentId: string, + sharedContext?: Context + ): Promise + async removeLineItemAdjustments( + selector: Partial, + sharedContext?: Context + ): Promise + + async removeLineItemAdjustments( + adjustmentIdsOrSelector: + | string + | string[] + | Partial, + @MedusaContext() sharedContext: Context = {} + ): Promise { + let ids: string[] = [] + if (isObject(adjustmentIdsOrSelector)) { + const adjustments = await this.listLineItemAdjustments( + { + ...adjustmentIdsOrSelector, + } as Partial, + { select: ["id"] }, + sharedContext + ) + + ids = adjustments.map((adj) => adj.id) + } else { + ids = Array.isArray(adjustmentIdsOrSelector) + ? adjustmentIdsOrSelector + : [adjustmentIdsOrSelector] + } + + await this.lineItemAdjustmentService_.delete(ids, sharedContext) + } } diff --git a/packages/cart/src/services/index.ts b/packages/cart/src/services/index.ts index b3c18ffa65e7d..c982c3e826ca3 100644 --- a/packages/cart/src/services/index.ts +++ b/packages/cart/src/services/index.ts @@ -2,5 +2,6 @@ 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 LineItemAdjustmentService } from "./line-item-adjustment" export { default as ShippingMethodService } from "./shipping-method" diff --git a/packages/cart/src/services/line-item-adjustment.ts b/packages/cart/src/services/line-item-adjustment.ts new file mode 100644 index 0000000000000..9ee93b2d95820 --- /dev/null +++ b/packages/cart/src/services/line-item-adjustment.ts @@ -0,0 +1,26 @@ +import { DAL } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { LineItemAdjustment } from "@models" +import { + CreateLineItemAdjustmentDTO, + UpdateLineItemAdjustmentDTO, +} from "@types" + +type InjectedDependencies = { + lineItemAdjustmentRepository: DAL.RepositoryService +} + +export default class LineItemAdjustmentService< + TEntity extends LineItemAdjustment = LineItemAdjustment +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { + create: CreateLineItemAdjustmentDTO + update: UpdateLineItemAdjustmentDTO + } +>(LineItemAdjustment) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/types/cart.ts b/packages/cart/src/types/cart.ts index 852159840abcd..0b0df6ac1cba0 100644 --- a/packages/cart/src/types/cart.ts +++ b/packages/cart/src/types/cart.ts @@ -1,3 +1,8 @@ +import { + CreateLineItemAdjustmentDTO, + UpdateLineItemAdjustmentDTO, +} from "./line-item-adjustment" + export interface CreateCartDTO { region_id?: string customer_id?: string @@ -15,4 +20,6 @@ export interface UpdateCartDTO { email?: string currency_code?: string metadata?: Record + + adjustments?: (CreateLineItemAdjustmentDTO | UpdateLineItemAdjustmentDTO)[] } diff --git a/packages/cart/src/types/index.ts b/packages/cart/src/types/index.ts index 3ee41da0c0a53..d5866dad06a01 100644 --- a/packages/cart/src/types/index.ts +++ b/packages/cart/src/types/index.ts @@ -3,6 +3,7 @@ import { Logger } from "@medusajs/types" export * from "./address" export * from "./cart" export * from "./line-item" +export * from "./line-item-adjustment" export * from "./shipping-method" export * from "./repositories" diff --git a/packages/cart/src/types/line-item-adjustment.ts b/packages/cart/src/types/line-item-adjustment.ts new file mode 100644 index 0000000000000..bb67befede095 --- /dev/null +++ b/packages/cart/src/types/line-item-adjustment.ts @@ -0,0 +1,13 @@ +export interface CreateLineItemAdjustmentDTO { + item_id: string + amount: number + code?: string + description?: string + promotion_id?: string + provider_id?: string +} + +export interface UpdateLineItemAdjustmentDTO + extends Partial { + id: string +} diff --git a/packages/cart/src/types/repositories.ts b/packages/cart/src/types/repositories.ts index 6df208fdaa813..2b38ef909cdbf 100644 --- a/packages/cart/src/types/repositories.ts +++ b/packages/cart/src/types/repositories.ts @@ -1,8 +1,12 @@ import { DAL } from "@medusajs/types" -import { Cart, LineItem, ShippingMethod } from "@models" +import { Cart, LineItem, LineItemAdjustment, ShippingMethod } from "@models" import { CreateAddressDTO, UpdateAddressDTO } from "./address" import { CreateCartDTO, UpdateCartDTO } from "./cart" import { CreateLineItemDTO, UpdateLineItemDTO } from "./line-item" +import { + CreateLineItemAdjustmentDTO, + UpdateLineItemAdjustmentDTO, +} from "./line-item-adjustment" import { CreateShippingMethodDTO, UpdateShippingMethodDTO, @@ -48,3 +52,13 @@ export interface IShippingMethodRepository< update: UpdateShippingMethodDTO } > {} + +export interface ILineItemAdjustmentRepository< + TEntity extends LineItemAdjustment = LineItemAdjustment +> extends DAL.RepositoryService< + TEntity, + { + create: CreateLineItemAdjustmentDTO + update: UpdateLineItemAdjustmentDTO + } + > {} diff --git a/packages/types/src/cart/common.ts b/packages/types/src/cart/common.ts index 508bfc8dc7733..08d9a567700c2 100644 --- a/packages/types/src/cart/common.ts +++ b/packages/types/src/cart/common.ts @@ -9,11 +9,15 @@ export interface AdjustmentLineDTO { /** * The code of the adjustment line */ - code: string + code?: string /** * The amount of the adjustment line */ amount: number + /** + * The ID of the associated cart + */ + cart_id: string /** * The description of the adjustment line */ @@ -43,11 +47,16 @@ export interface ShippingMethodAdjustmentLineDTO extends AdjustmentLineDTO { shipping_method: CartShippingMethodDTO } -export interface LineItemAdjustmentLineDTO extends AdjustmentLineDTO { +export interface LineItemAdjustmentDTO extends AdjustmentLineDTO { + /** + * The associated line item + * @expandable + */ + item: CartLineItemDTO /** * The associated line item */ - line_item: CartLineItemDTO + item_id: string } export interface TaxLineDTO { @@ -340,7 +349,7 @@ export interface CartLineItemDTO { * * @expandable */ - adjustments?: LineItemAdjustmentLineDTO[] + adjustments?: LineItemAdjustmentDTO[] /** * The associated cart. * @@ -494,6 +503,13 @@ export interface FilterableLineItemProps product_id?: string | string[] } +export interface FilterableLineItemAdjustmentProps + extends BaseFilterable { + id?: string | string[] + item_id?: string | string[] + promotion_id?: string | string[] + provider_id?: string | string[] +} export interface FilterableShippingMethodProps extends BaseFilterable { id?: string | string[] diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index 6416d0a7c97d0..98da700bbbe27 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -1,5 +1,6 @@ import { CartLineItemDTO } from "./common" +/** ADDRESS START */ export interface UpsertAddressDTO { customer_id?: string company?: string @@ -21,6 +22,9 @@ export interface UpdateAddressDTO extends UpsertAddressDTO { export interface CreateAddressDTO extends UpsertAddressDTO {} +/** ADDRESS END */ + +/** CART START */ export interface CreateCartDTO { region_id?: string customer_id?: string @@ -54,6 +58,9 @@ export interface UpdateCartDTO { metadata?: Record } +/** CART END */ + +/** TAXLINES START */ export interface CreateTaxLineDTO { description?: string tax_rate_id?: string @@ -62,25 +69,47 @@ export interface CreateTaxLineDTO { provider_id?: string } +export interface UpdateTaxLineDTO { + id: string + description?: string + tax_rate_id?: string + code?: string + rate?: number + provider_id?: string +} + +/** TAXLINES END */ + +/** ADJUSTMENT START */ export interface CreateAdjustmentDTO { - code: string + item_id: string + code?: string amount: number description?: string promotion_id?: string provider_id?: string } -export interface UpdateTaxLineDTO { +export interface UpdateAdjustmentDTO { id: string - description?: string - tax_rate_id?: string code?: string - rate?: number + amount: number + description?: string + promotion_id?: string provider_id?: string } -export interface UpdateAdjustmentDTO { - id: string +export interface CreateLineItemAdjustmentDTO extends CreateAdjustmentDTO { + item_id: string +} + +export interface UpdateLineItemAdjustmentDTO extends UpdateAdjustmentDTO { + item_id: string +} + +export interface UpsertLineItemAdjustmentDTO { + id?: string + item_id: string code?: string amount?: number description?: string @@ -88,6 +117,9 @@ export interface UpdateAdjustmentDTO { provider_id?: string } +/** ADJUSTMENT START */ + +/** LINE ITEMS START */ export interface CreateLineItemDTO { title: string subtitle?: string @@ -146,14 +178,23 @@ export interface UpdateLineItemDTO adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[] } +/** LINE ITEMS END */ + +/** SHIPPING METHODS START */ + export interface CreateShippingMethodDTO { name: string - cart_id: string - amount: number data?: Record + tax_lines?: CreateTaxLineDTO[] + adjustments?: CreateAdjustmentDTO[] +} +export interface CreateShippingMethodForSingleCartDTO { + name: string + amount: number + data?: Record tax_lines?: CreateTaxLineDTO[] adjustments?: CreateAdjustmentDTO[] } @@ -161,10 +202,10 @@ export interface CreateShippingMethodDTO { export interface UpdateShippingMethodDTO { id: string name?: string - amount?: number data?: Record - tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[] - adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[] + adjustments?: CreateAdjustmentDTO[] | UpdateAdjustmentDTO[] } + +/** SHIPPING METHODS END */ diff --git a/packages/types/src/cart/service.ts b/packages/types/src/cart/service.ts index 14e6222998d17..1af01d344d5d7 100644 --- a/packages/types/src/cart/service.ts +++ b/packages/types/src/cart/service.ts @@ -2,24 +2,29 @@ import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { - CartAddressDTO, - CartDTO, - CartLineItemDTO, - CartShippingMethodDTO, - FilterableAddressProps, - FilterableCartProps, - FilterableShippingMethodProps, + CartAddressDTO, + CartDTO, + CartLineItemDTO, + CartShippingMethodDTO, + FilterableAddressProps, + FilterableCartProps, + FilterableLineItemAdjustmentProps, + FilterableLineItemProps, + FilterableShippingMethodProps, + LineItemAdjustmentDTO, } from "./common" import { - CreateAddressDTO, - CreateCartDTO, - CreateLineItemDTO, - CreateLineItemForCartDTO, - CreateShippingMethodDTO, - UpdateAddressDTO, - UpdateCartDTO, - UpdateLineItemDTO, - UpdateLineItemWithSelectorDTO, + CreateAddressDTO, + CreateAdjustmentDTO, + CreateCartDTO, + CreateLineItemDTO, + CreateLineItemForCartDTO, + CreateShippingMethodDTO, + UpdateAddressDTO, + UpdateCartDTO, + UpdateLineItemDTO, + UpdateLineItemWithSelectorDTO, + UpsertLineItemAdjustmentDTO } from "./mutations" export interface ICartModuleService extends IModuleService { @@ -77,7 +82,19 @@ export interface ICartModuleService extends IModuleService { deleteAddresses(ids: string[], sharedContext?: Context): Promise deleteAddresses(ids: string, sharedContext?: Context): Promise - addLineItems(data: CreateLineItemForCartDTO): Promise + retrieveLineItem( + itemId: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listLineItems( + filters: FilterableLineItemProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + addLineItems(data: CreateLineItemForCartDTO): Promise addLineItems(data: CreateLineItemForCartDTO[]): Promise addLineItems( cartId: string, @@ -136,4 +153,40 @@ export interface ICartModuleService extends IModuleService { selector: Partial, sharedContext?: Context ): Promise + + listLineItemAdjustments( + filters: FilterableLineItemAdjustmentProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + addLineItemAdjustments( + data: CreateAdjustmentDTO[] + ): Promise + addLineItemAdjustments( + data: CreateAdjustmentDTO + ): Promise + addLineItemAdjustments( + cartId: string, + data: CreateAdjustmentDTO[] + ): Promise + + setLineItemAdjustments( + cartId: string, + data: UpsertLineItemAdjustmentDTO[], + sharedContext?: Context + ): Promise + + removeLineItemAdjustments( + adjustmentIds: string[], + sharedContext?: Context + ): Promise + removeLineItemAdjustments( + adjustmentIds: string, + sharedContext?: Context + ): Promise + removeLineItemAdjustments( + selector: Partial, + sharedContext?: Context + ): Promise } diff --git a/packages/utils/src/modules-sdk/abstract-service-factory.ts b/packages/utils/src/modules-sdk/abstract-service-factory.ts index c877acd549150..47afc4c7eb635 100644 --- a/packages/utils/src/modules-sdk/abstract-service-factory.ts +++ b/packages/utils/src/modules-sdk/abstract-service-factory.ts @@ -1,16 +1,16 @@ import { Context, - FilterQuery as InternalFilterQuery, FindConfig, + FilterQuery as InternalFilterQuery, } from "@medusajs/types" import { EntitySchema } from "@mikro-orm/core" import { EntityClass } from "@mikro-orm/core/typings" import { + MedusaError, doNotForceTransaction, isDefined, isString, lowerCaseFirst, - MedusaError, shouldForceTransaction, upperCaseFirst, } from "../common"