feat(cart): Line items operations (#6066)

This commit is contained in:
Oli Juhl
2024-01-16 15:44:49 +01:00
committed by GitHub
parent 6315a61189
commit a5e8bf06c6
18 changed files with 973 additions and 136 deletions

View File

@@ -1,5 +1,4 @@
import { ICartModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initialize } from "../../../../src/initialize"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
@@ -7,11 +6,9 @@ jest.setTimeout(30000)
describe("Cart Module Service", () => {
let service: ICartModuleService
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
service = await initialize({
database: {
@@ -242,4 +239,368 @@ describe("Cart Module Service", () => {
expect(address).toBe(undefined)
})
})
describe("addLineItems", () => {
it("should add a line item to cart succesfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const items = await service.addLineItems(createdCart.id, [
{
quantity: 1,
unit_price: 100,
title: "test",
},
])
const cart = await service.retrieve(createdCart.id, {
relations: ["items"],
})
expect(items[0]).toEqual(
expect.objectContaining({
title: "test",
quantity: 1,
unit_price: 100,
})
)
expect(cart.items?.length).toBe(1)
})
it("should add multiple line items to cart succesfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
await service.addLineItems([
{
quantity: 1,
unit_price: 100,
title: "test",
cart_id: createdCart.id,
},
{
quantity: 2,
unit_price: 200,
title: "test-2",
cart_id: createdCart.id,
},
])
const cart = await service.retrieve(createdCart.id, {
relations: ["items"],
})
expect(cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: "test",
quantity: 1,
unit_price: 100,
}),
expect.objectContaining({
title: "test-2",
quantity: 2,
unit_price: 200,
}),
])
)
expect(cart.items?.length).toBe(2)
})
it("should add multiple line items to multiple carts succesfully", async () => {
let [eurCart] = await service.create([
{
currency_code: "eur",
},
])
let [usdCart] = await service.create([
{
currency_code: "usd",
},
])
const items = await service.addLineItems([
{
cart_id: eurCart.id,
quantity: 1,
unit_price: 100,
title: "test",
},
{
cart_id: usdCart.id,
quantity: 1,
unit_price: 100,
title: "test",
},
])
const carts = await service.list(
{ id: [eurCart.id, usdCart.id] },
{ relations: ["items"] }
)
eurCart = carts.find((c) => c.currency_code === "eur")!
usdCart = carts.find((c) => c.currency_code === "usd")!
const eurItems = items.filter((i) => i.cart_id === eurCart.id)
const usdItems = items.filter((i) => i.cart_id === usdCart.id)
expect(eurCart.items![0].id).toBe(eurItems[0].id)
expect(usdCart.items![0].id).toBe(usdItems[0].id)
expect(eurCart.items?.length).toBe(1)
expect(usdCart.items?.length).toBe(1)
})
it("should throw if cart does not exist", async () => {
const error = await service
.addLineItems("foo", [
{
quantity: 1,
unit_price: 100,
title: "test",
tax_lines: [],
},
])
.catch((e) => e)
expect(error.message).toContain("Cart with id: foo was not found")
})
it("should throw an error when required params are not passed adding to a single cart", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const error = await service
.addLineItems(createdCart.id, [
{
quantity: 1,
title: "test",
},
] as any)
.catch((e) => e)
expect(error.message).toContain(
"Value for LineItem.unit_price is required, 'undefined' found"
)
})
it("should throw a generic error when required params are not passed using bulk add method", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const error = await service
.addLineItems([
{
cart_id: createdCart.id,
quantity: 1,
title: "test",
},
] as any)
.catch((e) => e)
expect(error.message).toContain(
"Value for LineItem.unit_price is required, 'undefined' found"
)
})
})
describe("updateLineItems", () => {
it("should update a line item in cart succesfully with selector approach", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [item] = await service.addLineItems(createdCart.id, [
{
quantity: 1,
unit_price: 100,
title: "test",
tax_lines: [],
},
])
expect(item.title).toBe("test")
const [updatedItem] = await service.updateLineItems(
{ cart_id: createdCart.id },
{
title: "test2",
}
)
expect(updatedItem.title).toBe("test2")
})
it("should update a line item in cart succesfully with id approach", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [item] = await service.addLineItems(createdCart.id, [
{
quantity: 1,
unit_price: 100,
title: "test",
tax_lines: [],
},
])
expect(item.title).toBe("test")
const updatedItem = await service.updateLineItems(
item.id,
{
title: "test2",
}
)
expect(updatedItem.title).toBe("test2")
})
it("should update line items in carts succesfully with multi-selector approach", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const items = await service.addLineItems(createdCart.id, [
{
quantity: 1,
unit_price: 100,
title: "test",
},
{
quantity: 2,
unit_price: 200,
title: "other-test",
},
])
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: "test",
quantity: 1,
unit_price: 100,
}),
expect.objectContaining({
title: "other-test",
quantity: 2,
unit_price: 200,
}),
])
)
const itemOne = items.find((i) => i.title === "test")
const itemTwo = items.find((i) => i.title === "other-test")
const updatedItems = await service.updateLineItems([
{
selector: { cart_id: createdCart.id },
data: {
title: "changed-test",
}
},
{
selector: { id: itemTwo!.id },
data: {
title: "changed-other-test",
}
},
])
expect(updatedItems).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: "changed-test",
quantity: 1,
unit_price: 100,
}),
expect.objectContaining({
title: "changed-other-test",
quantity: 2,
unit_price: 200,
}),
])
)
})
})
describe("removeLineItems", () => {
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",
tax_lines: [],
},
])
expect(item.title).toBe("test")
await service.removeLineItems([item.id])
const cart = await service.retrieve(createdCart.id, {
relations: ["items"],
})
expect(cart.items?.length).toBe(0)
})
it("should remove multiple line items succesfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [item, item2] = await service.addLineItems(createdCart.id, [
{
quantity: 1,
unit_price: 100,
title: "test",
},
{
quantity: 1,
unit_price: 100,
title: "test-2",
},
])
await service.removeLineItems([item.id, item2.id])
const cart = await service.retrieve(createdCart.id, {
relations: ["items"],
})
expect(cart.items?.length).toBe(0)
})
})
})

View File

@@ -20,6 +20,7 @@ export default async ({
container.register({
cartService: asClass(defaultServices.CartService).singleton(),
addressService: asClass(defaultServices.AddressService).singleton(),
lineItemService: asClass(defaultServices.LineItemService).singleton(),
})
if (customRepositories) {
@@ -38,5 +39,6 @@ function loadDefaultRepositories({ container }) {
baseRepository: asClass(defaultRepositories.BaseRepository).singleton(),
cartRepository: asClass(defaultRepositories.CartRepository).singleton(),
addressRepository: asClass(defaultRepositories.AddressRepository).singleton(),
lineItemRepository: asClass(defaultRepositories.LineItemRepository).singleton(),
})
}

View File

@@ -11,7 +11,7 @@ import {
OneToMany,
OptionalProps,
PrimaryKey,
Property,
Property
} from "@mikro-orm/core"
import Address from "./address"
import LineItem from "./line-item"
@@ -78,7 +78,6 @@ export default class Cart {
@OneToMany(() => ShippingMethod, (shippingMethod) => shippingMethod.cart, {
cascade: [Cascade.REMOVE],
})
shipping_methods = new Collection<ShippingMethod>(this)

View File

@@ -22,6 +22,7 @@ type OptionalLineItemProps =
| "is_tax_inclusive"
| "compare_at_unit_price"
| "requires_shipping"
| "cart"
| DAL.EntityDateColumns
@Entity({ tableName: "cart_line_item" })
@@ -31,12 +32,15 @@ export default class LineItem {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
cart_id: string
@ManyToOne(() => Cart, {
onDelete: "cascade",
index: "IDX_line_item_cart_id",
fieldName: "cart_id",
nullable: true,
})
cart!: Cart
cart?: Cart | null
@Property({ columnType: "text" })
title: string
@@ -47,7 +51,7 @@ export default class LineItem {
@Property({ columnType: "text", nullable: true })
thumbnail?: string | null
@Property({ columnType: "text" })
@Property({ columnType: "integer" })
quantity: number
@Property({
@@ -90,13 +94,13 @@ export default class LineItem {
@Property({ columnType: "jsonb", nullable: true })
variant_option_values?: Record<string, unknown> | null
@Property({ columnType: "boolean" })
@Property({ columnType: "boolean", default: true })
requires_shipping = true
@Property({ columnType: "boolean" })
@Property({ columnType: "boolean", default: true })
is_discountable = true
@Property({ columnType: "boolean" })
@Property({ columnType: "boolean", default: false })
is_tax_inclusive = false
@Property({ columnType: "numeric", nullable: true })
@@ -147,7 +151,7 @@ export default class LineItem {
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
created_at?: Date
@Property({
onCreate: () => new Date(),
@@ -155,7 +159,7 @@ export default class LineItem {
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
updated_at?: Date
@BeforeCreate()
onCreate() {

View File

@@ -1,6 +1,4 @@
import { Context } from "@medusajs/types"
import { DALUtils } from "@medusajs/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Address } from "@models"
import { CreateAddressDTO, UpdateAddressDTO } from "../types"
@@ -8,25 +6,6 @@ export class AddressRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
Address,
{
create: CreateAddressDTO
update: UpdateAddressDTO
}
>(Address) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async update(
data: { address: Address; update: UpdateAddressDTO }[],
context: Context = {}
): Promise<Address[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const entities = data.map(({ address, update }) => {
return manager.assign(address, update)
})
manager.persist(entities)
return entities
}
}
>(Address) {}

View File

@@ -1,4 +1,5 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export * from "./address"
export * from "./cart"
export * from "./line-item"

View File

@@ -0,0 +1,11 @@
import { DALUtils } from "@medusajs/utils"
import { LineItem } from "@models"
import { CreateLineItemDTO, UpdateLineItemDTO } from "../types"
export class LineItemRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
LineItem,
{
create: CreateLineItemDTO
update: UpdateLineItemDTO
}
>(LineItem) {}

View File

@@ -1,5 +1,6 @@
import {
AddressDTO,
CartAddressDTO,
Context,
DAL,
FilterableAddressProps,
@@ -46,7 +47,7 @@ export default class AddressService<TEntity extends Address = Address> {
@InjectManager("addressRepository_")
async list(
filters: FilterableAddressProps = {},
config: FindConfig<AddressDTO> = {},
config: FindConfig<CartAddressDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<Address>(filters, config)
@@ -97,7 +98,7 @@ export default class AddressService<TEntity extends Address = Address> {
existingAddresses.map<[string, Address]>((addr) => [addr.id, addr])
)
const updates: { address: Address; update: UpdateAddressDTO }[] = []
const updates: UpdateAddressDTO[] = []
for (const update of data) {
const address = existingAddressesMap.get(update.id)
@@ -109,7 +110,7 @@ export default class AddressService<TEntity extends Address = Address> {
)
}
updates.push({ address, update })
updates.push({ ...update, id: address.id })
}
return (await (this.addressRepository_ as AddressRepository).update(

View File

@@ -1,47 +1,53 @@
import {
AddressDTO,
CartAddressDTO,
CartDTO,
Context,
CreateCartDTO,
DAL,
FilterableCartProps,
FindConfig,
ICartModuleService,
InternalModuleDeclaration,
ModuleJoinerConfig,
UpdateCartDTO,
} from "@medusajs/types"
import { FilterableAddressProps } from "@medusajs/types"
import { CartTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
isObject,
isString,
} from "@medusajs/utils"
import { CreateAddressDTO, UpdateAddressDTO } from "@types"
import { LineItem } from "@models"
import { UpdateLineItemDTO } from "@types"
import { joinerConfig } from "../joiner-config"
import AddressService from "./address"
import CartService from "./cart"
import * as services from "../services"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
cartService: CartService
addressService: AddressService
cartService: services.CartService
addressService: services.AddressService
lineItemService: services.LineItemService
}
export default class CartModuleService implements ICartModuleService {
protected baseRepository_: DAL.RepositoryService
protected cartService_: CartService
protected addressService_: AddressService
protected cartService_: services.CartService
protected addressService_: services.AddressService
protected lineItemService_: services.LineItemService
constructor(
{ baseRepository, cartService, addressService }: InjectedDependencies,
{
baseRepository,
cartService,
addressService,
lineItemService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.cartService_ = cartService
this.addressService_ = addressService
this.lineItemService_ = lineItemService
}
__joinerConfig(): ModuleJoinerConfig {
@@ -51,25 +57,25 @@ export default class CartModuleService implements ICartModuleService {
@InjectManager("baseRepository_")
async retrieve(
id: string,
config: FindConfig<CartDTO> = {},
config: FindConfig<CartTypes.CartDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<CartDTO> {
): Promise<CartTypes.CartDTO> {
const cart = await this.cartService_.retrieve(id, config, sharedContext)
return await this.baseRepository_.serialize<CartDTO>(cart, {
return await this.baseRepository_.serialize<CartTypes.CartDTO>(cart, {
populate: true,
})
}
@InjectManager("baseRepository_")
async list(
filters: FilterableCartProps = {},
config: FindConfig<CartDTO> = {},
filters: CartTypes.FilterableCartProps = {},
config: FindConfig<CartTypes.CartDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<CartDTO[]> {
): Promise<CartTypes.CartDTO[]> {
const carts = await this.cartService_.list(filters, config, sharedContext)
return await this.baseRepository_.serialize<CartDTO[]>(carts, {
return this.baseRepository_.serialize<CartTypes.CartDTO[]>(carts, {
populate: true,
})
}
@@ -77,9 +83,9 @@ export default class CartModuleService implements ICartModuleService {
@InjectManager("baseRepository_")
async listAndCount(
filters: FilterableCartProps = {},
config: FindConfig<CartDTO> = {},
config: FindConfig<CartTypes.CartDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[CartDTO[], number]> {
): Promise<[CartTypes.CartDTO[], number]> {
const [carts, count] = await this.cartService_.listAndCount(
filters,
config,
@@ -87,7 +93,7 @@ export default class CartModuleService implements ICartModuleService {
)
return [
await this.baseRepository_.serialize<CartDTO[]>(carts, {
await this.baseRepository_.serialize<CartTypes.CartDTO[]>(carts, {
populate: true,
}),
count,
@@ -95,17 +101,20 @@ export default class CartModuleService implements ICartModuleService {
}
async create(
data: CreateCartDTO[],
data: CartTypes.CreateCartDTO[],
sharedContext?: Context
): Promise<CartDTO[]>
): Promise<CartTypes.CartDTO[]>
async create(data: CreateCartDTO, sharedContext?: Context): Promise<CartDTO>
async create(
data: CartTypes.CreateCartDTO,
sharedContext?: Context
): Promise<CartTypes.CartDTO>
@InjectManager("baseRepository_")
async create(
data: CreateCartDTO[] | CreateCartDTO,
data: CartTypes.CreateCartDTO[] | CartTypes.CreateCartDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<CartDTO[] | CartDTO> {
): Promise<CartTypes.CartDTO[] | CartTypes.CartDTO> {
const input = Array.isArray(data) ? data : [data]
const carts = await this.create_(input, sharedContext)
@@ -118,29 +127,34 @@ export default class CartModuleService implements ICartModuleService {
sharedContext
)
return (Array.isArray(data) ? result : result[0]) as CartDTO | CartDTO[]
return (Array.isArray(data) ? result : result[0]) as
| CartTypes.CartDTO
| CartTypes.CartDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async create_(
data: CreateCartDTO[],
data: CartTypes.CreateCartDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.cartService_.create(data, sharedContext)
}
async update(
data: UpdateCartDTO[],
data: CartTypes.UpdateCartDTO[],
sharedContext?: Context
): Promise<CartDTO[]>
): Promise<CartTypes.CartDTO[]>
async update(data: UpdateCartDTO, sharedContext?: Context): Promise<CartDTO>
async update(
data: CartTypes.UpdateCartDTO,
sharedContext?: Context
): Promise<CartTypes.CartDTO>
@InjectManager("baseRepository_")
async update(
data: UpdateCartDTO[] | UpdateCartDTO,
data: CartTypes.UpdateCartDTO[] | CartTypes.UpdateCartDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<CartDTO[] | CartDTO> {
): Promise<CartTypes.CartDTO[] | CartTypes.CartDTO> {
const input = Array.isArray(data) ? data : [data]
const carts = await this.update_(input, sharedContext)
@@ -152,12 +166,14 @@ export default class CartModuleService implements ICartModuleService {
sharedContext
)
return (Array.isArray(data) ? result : result[0]) as CartDTO | CartDTO[]
return (Array.isArray(data) ? result : result[0]) as
| CartTypes.CartDTO
| CartTypes.CartDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async update_(
data: UpdateCartDTO[],
data: CartTypes.UpdateCartDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.cartService_.update(data, sharedContext)
@@ -178,8 +194,8 @@ export default class CartModuleService implements ICartModuleService {
@InjectManager("baseRepository_")
async listAddresses(
filters: FilterableAddressProps = {},
config: FindConfig<AddressDTO> = {},
filters: CartTypes.FilterableAddressProps = {},
config: FindConfig<CartTypes.CartAddressDTO> = {},
@MedusaContext() sharedContext: Context = {}
) {
const addresses = await this.addressService_.list(
@@ -188,19 +204,260 @@ export default class CartModuleService implements ICartModuleService {
sharedContext
)
return await this.baseRepository_.serialize<CartAddressDTO[]>(addresses, {
populate: true,
})
return await this.baseRepository_.serialize<CartTypes.CartAddressDTO[]>(
addresses,
{
populate: true,
}
)
}
async createAddresses(data: CreateAddressDTO, sharedContext?: Context)
async createAddresses(data: CreateAddressDTO[], sharedContext?: Context)
@InjectManager("baseRepository_")
async retrieveLineItem(
itemId: string,
config: FindConfig<CartTypes.CartLineItemDTO> = {},
@MedusaContext() sharedContext: Context = {}
) {
const item = await this.lineItemService_.retrieve(
itemId,
config,
sharedContext
)
return await this.baseRepository_.serialize<CartTypes.CartLineItemDTO[]>(
item,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listLineItems(
filters: CartTypes.FilterableLineItemProps = {},
config: FindConfig<CartTypes.CartLineItemDTO> = {},
@MedusaContext() sharedContext: Context = {}
) {
const items = await this.lineItemService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<CartTypes.CartLineItemDTO[]>(
items,
{
populate: true,
}
)
}
addLineItems(
data: CartTypes.CreateLineItemForCartDTO
): Promise<CartTypes.CartLineItemDTO>
addLineItems(
data: CartTypes.CreateLineItemForCartDTO[]
): Promise<CartTypes.CartLineItemDTO[]>
addLineItems(
cartId: string,
items: CartTypes.CreateLineItemDTO[],
sharedContext?: Context
): Promise<CartTypes.CartLineItemDTO[]>
@InjectManager("baseRepository_")
async addLineItems(
cartIdOrData:
| string
| CartTypes.CreateLineItemForCartDTO[]
| CartTypes.CreateLineItemForCartDTO,
data?: CartTypes.CreateLineItemDTO[] | CartTypes.CreateLineItemDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<CartTypes.CartLineItemDTO[] | CartTypes.CartLineItemDTO> {
let items: LineItem[] = []
if (isString(cartIdOrData)) {
items = await this.addLineItems_(
cartIdOrData,
data as CartTypes.CreateLineItemDTO[],
sharedContext
)
} else {
const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData]
items = await this.addLineItemsBulk_(data, sharedContext)
}
return await this.baseRepository_.serialize<CartTypes.CartLineItemDTO[]>(
items,
{
populate: true,
}
)
}
@InjectTransactionManager("baseRepository_")
protected async addLineItems_(
cartId: string,
items: CartTypes.CreateLineItemDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<LineItem[]> {
const cart = await this.retrieve(cartId, { select: ["id"] }, sharedContext)
const toUpdate = items.map((item) => {
return {
...item,
cart_id: cart.id,
}
})
return await this.addLineItemsBulk_(toUpdate, sharedContext)
}
@InjectTransactionManager("baseRepository_")
protected async addLineItemsBulk_(
data: CartTypes.CreateLineItemForCartDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<LineItem[]> {
return await this.lineItemService_.create(data, sharedContext)
}
updateLineItems(
data: CartTypes.UpdateLineItemWithSelectorDTO[]
): Promise<CartTypes.CartLineItemDTO[]>
updateLineItems(
selector: Partial<CartTypes.CartLineItemDTO>,
data: CartTypes.UpdateLineItemDTO,
sharedContext?: Context
): Promise<CartTypes.CartLineItemDTO[]>
updateLineItems(
lineItemId: string,
data: Partial<CartTypes.UpdateLineItemDTO>,
sharedContext?: Context
): Promise<CartTypes.CartLineItemDTO>
@InjectManager("baseRepository_")
async updateLineItems(
lineItemIdOrDataOrSelector:
| string
| CartTypes.UpdateLineItemWithSelectorDTO[]
| Partial<CartTypes.CartLineItemDTO>,
data?: CartTypes.UpdateLineItemDTO | Partial<CartTypes.UpdateLineItemDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<CartTypes.CartLineItemDTO[] | CartTypes.CartLineItemDTO> {
let items: LineItem[] = []
if (isString(lineItemIdOrDataOrSelector)) {
const item = await this.updateLineItem_(
lineItemIdOrDataOrSelector,
data as Partial<CartTypes.UpdateLineItemDTO>,
sharedContext
)
return await this.baseRepository_.serialize<CartTypes.CartLineItemDTO>(
item,
{
populate: true,
}
)
}
const toUpdate = Array.isArray(lineItemIdOrDataOrSelector)
? lineItemIdOrDataOrSelector
: [
{
selector: lineItemIdOrDataOrSelector,
data: data,
} as CartTypes.UpdateLineItemWithSelectorDTO,
]
items = await this.updateLineItemsWithSelector_(toUpdate, sharedContext)
return await this.baseRepository_.serialize<CartTypes.CartLineItemDTO[]>(
items,
{
populate: true,
}
)
}
@InjectTransactionManager("baseRepository_")
protected async updateLineItem_(
lineItemId: string,
data: Partial<CartTypes.UpdateLineItemDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<LineItem> {
const [item] = await this.lineItemService_.update(
[{ id: lineItemId, ...data }],
sharedContext
)
return item
}
@InjectTransactionManager("baseRepository_")
protected async updateLineItemsWithSelector_(
updates: CartTypes.UpdateLineItemWithSelectorDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<LineItem[]> {
let toUpdate: UpdateLineItemDTO[] = []
for (const { selector, data } of updates) {
const items = await this.listLineItems({ ...selector }, {}, sharedContext)
items.forEach((item) => {
toUpdate.push({
...data,
id: item.id,
})
})
}
return await this.lineItemService_.update(toUpdate, sharedContext)
}
async removeLineItems(
itemIds: string[],
sharedContext?: Context
): Promise<void>
async removeLineItems(itemIds: string, sharedContext?: Context): Promise<void>
async removeLineItems(
selector: Partial<CartTypes.CartLineItemDTO>,
sharedContext?: Context
): Promise<void>
@InjectTransactionManager("baseRepository_")
async removeLineItems(
itemIdsOrSelector: string | string[] | Partial<CartTypes.CartLineItemDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
let toDelete: string[] = []
if (isObject(itemIdsOrSelector)) {
const items = await this.listLineItems(
{ ...itemIdsOrSelector } as Partial<CartTypes.CartLineItemDTO>,
{},
sharedContext
)
toDelete = items.map((item) => item.id)
} else {
toDelete = Array.isArray(itemIdsOrSelector)
? itemIdsOrSelector
: [itemIdsOrSelector]
}
await this.lineItemService_.delete(toDelete, sharedContext)
}
async createAddresses(
data: CartTypes.CreateAddressDTO,
sharedContext?: Context
): Promise<CartTypes.CartAddressDTO>
async createAddresses(
data: CartTypes.CreateAddressDTO[],
sharedContext?: Context
): Promise<CartTypes.CartAddressDTO[]>
@InjectManager("baseRepository_")
async createAddresses(
data: CreateAddressDTO[] | CreateAddressDTO,
data: CartTypes.CreateAddressDTO[] | CartTypes.CreateAddressDTO,
@MedusaContext() sharedContext: Context = {}
) {
): Promise<CartTypes.CartAddressDTO | CartTypes.CartAddressDTO[]> {
const input = Array.isArray(data) ? data : [data]
const addresses = await this.createAddresses_(input, sharedContext)
@@ -211,26 +468,32 @@ export default class CartModuleService implements ICartModuleService {
)
return (Array.isArray(data) ? result : result[0]) as
| AddressDTO
| AddressDTO[]
| CartTypes.CartAddressDTO
| CartTypes.CartAddressDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async createAddresses_(
data: CreateAddressDTO[],
data: CartTypes.CreateAddressDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.addressService_.create(data, sharedContext)
}
async updateAddresses(data: UpdateAddressDTO, sharedContext?: Context)
async updateAddresses(data: UpdateAddressDTO[], sharedContext?: Context)
async updateAddresses(
data: CartTypes.UpdateAddressDTO,
sharedContext?: Context
): Promise<CartTypes.CartAddressDTO>
async updateAddresses(
data: CartTypes.UpdateAddressDTO[],
sharedContext?: Context
): Promise<CartTypes.CartAddressDTO[]>
@InjectManager("baseRepository_")
async updateAddresses(
data: UpdateAddressDTO[] | UpdateAddressDTO,
data: CartTypes.UpdateAddressDTO[] | CartTypes.UpdateAddressDTO,
@MedusaContext() sharedContext: Context = {}
) {
): Promise<CartTypes.CartAddressDTO | CartTypes.CartAddressDTO[]> {
const input = Array.isArray(data) ? data : [data]
const addresses = await this.updateAddresses_(input, sharedContext)
@@ -241,26 +504,26 @@ export default class CartModuleService implements ICartModuleService {
)
return (Array.isArray(data) ? result : result[0]) as
| AddressDTO
| AddressDTO[]
| CartTypes.CartAddressDTO
| CartTypes.CartAddressDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async updateAddresses_(
data: UpdateAddressDTO[],
data: CartTypes.UpdateAddressDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.addressService_.update(data, sharedContext)
}
async deleteAddresses(ids: string[], sharedContext?: Context)
async deleteAddresses(ids: string, sharedContext?: Context)
async deleteAddresses(ids: string[], sharedContext?: Context): Promise<void>
async deleteAddresses(ids: string, sharedContext?: Context): Promise<void>
@InjectTransactionManager("baseRepository_")
async deleteAddresses(
ids: string[] | string,
@MedusaContext() sharedContext: Context = {}
) {
): Promise<void> {
const addressIds = Array.isArray(ids) ? ids : [ids]
await this.addressService_.delete(addressIds, sharedContext)
}

View File

@@ -1,4 +1,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"

View File

@@ -0,0 +1,128 @@
import {
CartLineItemDTO,
Context,
DAL,
FilterableLineItemProps,
FindConfig,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { LineItem } from "@models"
import { LineItemRepository } from "@repositories"
import { CreateLineItemDTO, UpdateLineItemDTO } from "../types"
type InjectedDependencies = {
lineItemRepository: DAL.RepositoryService
}
export default class LineItemService<TEntity extends LineItem = LineItem> {
protected readonly lineItemRepository_: DAL.RepositoryService<LineItem>
constructor({ lineItemRepository }: InjectedDependencies) {
this.lineItemRepository_ = lineItemRepository
}
@InjectManager("lineItemRepository_")
async retrieve(
id: string,
config: FindConfig<CartLineItemDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<LineItem, CartLineItemDTO>({
id: id,
entityName: LineItem.name,
repository: this.lineItemRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("lineItemRepository_")
async list(
filters: FilterableLineItemProps = {},
config: FindConfig<CartLineItemDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<LineItem>(filters, config)
return (await this.lineItemRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("lineItemRepository_")
async listAndCount(
filters: FilterableLineItemProps = {},
config: FindConfig<CartLineItemDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<LineItem>(filters, config)
return (await this.lineItemRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("lineItemRepository_")
async create(
data: CreateLineItemDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.lineItemRepository_ as LineItemRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager("lineItemRepository_")
async update(
data: UpdateLineItemDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const existingLines = await this.list(
{ id: [...data.map((d) => d.id)] },
{},
sharedContext
)
const existingLinesMap = new Map(
existingLines.map<[string, LineItem]>((li) => [li.id, li])
)
const updates: UpdateLineItemDTO[] = []
for (const update of data) {
const lineItem = existingLinesMap.get(update.id)
if (!lineItem) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Line item with id "${update.id}" not found`
)
}
updates.push({ ...update, id: lineItem.id })
}
return (await (this.lineItemRepository_ as LineItemRepository).update(
updates,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager("lineItemRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.lineItemRepository_.delete(ids, sharedContext)
}
}

View File

@@ -1,6 +1,7 @@
import { Logger } from "@medusajs/types"
export * from "./address"
export * from "./cart"
export * from "./line-item"
export type InitializeModuleInjectableDependencies = {
logger?: Logger

View File

@@ -0,0 +1,37 @@
interface PartialUpsertLineItemDTO {
subtitle?: string
thumbnail?: string
product_id?: string
product_title?: string
product_description?: string
product_subtitle?: string
product_type?: string
product_collection?: string
product_handle?: string
variant_id?: string
variant_sku?: string
variant_barcode?: string
variant_title?: string
variant_option_values?: Record<string, unknown>
requires_shipping?: boolean
is_discountable?: boolean
is_tax_inclusive?: boolean
compare_at_unit_price?: number
}
export interface CreateLineItemDTO extends PartialUpsertLineItemDTO {
title: string
quantity: number
unit_price: number
cart_id: string
}
export interface UpdateLineItemDTO
extends PartialUpsertLineItemDTO,
Partial<CreateLineItemDTO> {
id: string
}

View File

@@ -1,4 +1,4 @@
import { AddressDTO, CustomerDTO, RegionDTO, legacy__CartDTO } from "@medusajs/types"
import { AddressDTO, CustomerDTO, RegionDTO, legacy_CartDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
enum Aliases {
@@ -34,7 +34,7 @@ type HandlerInputData = {
}
type HandlerOutputData = {
cart: legacy__CartDTO
cart: legacy_CartDTO
}
export async function createCart({

View File

@@ -1,4 +1,4 @@
import { legacy__CartDTO } from "@medusajs/types"
import { legacy_CartDTO } from "@medusajs/types"
import { WorkflowArguments } from "@medusajs/workflows-sdk"
type HandlerInputData = {
@@ -22,7 +22,7 @@ export async function retrieveCart({
container,
context,
data,
}: WorkflowArguments<HandlerInputData>): Promise<legacy__CartDTO> {
}: WorkflowArguments<HandlerInputData>): Promise<legacy_CartDTO> {
const { manager } = context
const cartService = container.resolve("cartService")

View File

@@ -336,6 +336,16 @@ export interface CartLineItemDTO {
* @expandable
*/
adjustments?: LineItemAdjustmentLineDTO[]
/**
* The associated cart.
*
* @expandable
*/
cart: CartDTO
/**
* The ID of the associated cart.
*/
cart_id: string
/**
* Holds custom data in key-value pairs.
*/
@@ -470,12 +480,20 @@ export interface FilterableAddressProps
id?: string | string[]
}
export interface FilterableLineItemProps
extends BaseFilterable<FilterableLineItemProps> {
id?: string | string[]
cart_id?: string | string[]
title?: string
variant_id?: string | string[]
product_id?: string | string[]
}
/**
* TODO: Remove this in favor of CartDTO, when module is released
* @deprecated Use CartDTO instead
*/
export type legacy__CartDTO = {
export type legacy_CartDTO = {
id?: string
email?: string
billing_address_id?: string

View File

@@ -1,3 +1,5 @@
import { CartLineItemDTO } from "./common"
export interface UpsertAddressDTO {
customer_id?: string
company?: string
@@ -93,6 +95,8 @@ export interface CreateLineItemDTO {
subtitle?: string
thumbnail?: string
cart_id?: string
quantity: number
product_id?: string
@@ -116,24 +120,30 @@ export interface CreateLineItemDTO {
compare_at_unit_price?: number
unit_price: number
tax_lines: CreateLineItemTaxLineDTO[]
adjustments: CreateLineItemAdjustmentDTO[]
tax_lines?: CreateLineItemTaxLineDTO[]
adjustments?: CreateLineItemAdjustmentDTO[]
}
export interface CreateLineItemForCartDTO extends CreateLineItemDTO {
cart_id: string
}
export interface UpdateLineItemWithSelectorDTO {
selector: Partial<CartLineItemDTO>
data: Partial<UpdateLineItemDTO>
}
export interface UpdateLineItemDTO
extends Omit<CreateLineItemDTO, "tax_lines" | "adjustments"> {
extends Omit<
CreateLineItemDTO,
"tax_lines" | "adjustments" | "title" | "quantity" | "unit_price"
> {
id: string
tax_lines: UpdateLineItemTaxLineDTO[] | CreateLineItemTaxLineDTO[]
adjustments: UpdateLineItemAdjustmentDTO[] | CreateLineItemAdjustmentDTO[]
}
title?: string
quantity?: number
unit_price?: number
export interface AddLineItemsDTO {
cart_id: string
items: CreateLineItemDTO[]
}
export interface UpdateLineItemsDTO {
cart_id: string
items: UpdateLineItemDTO[]
tax_lines?: UpdateLineItemTaxLineDTO[] | CreateLineItemTaxLineDTO[]
adjustments?: UpdateLineItemAdjustmentDTO[] | CreateLineItemAdjustmentDTO[]
}

View File

@@ -1,13 +1,22 @@
import { AddressDTO } from "../address"
import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import { CartAddressDTO, CartDTO, FilterableAddressProps, FilterableCartProps } from "./common"
import {
CartAddressDTO,
CartDTO,
CartLineItemDTO,
FilterableAddressProps,
FilterableCartProps,
} from "./common"
import {
CreateAddressDTO,
CreateCartDTO,
CreateLineItemDTO,
CreateLineItemForCartDTO,
UpdateAddressDTO,
UpdateCartDTO,
UpdateLineItemDTO,
UpdateLineItemWithSelectorDTO,
} from "./mutations"
export interface ICartModuleService extends IModuleService {
@@ -40,7 +49,7 @@ export interface ICartModuleService extends IModuleService {
listAddresses(
filters?: FilterableAddressProps,
config?: FindConfig<AddressDTO>,
config?: FindConfig<CartAddressDTO>,
sharedContext?: Context
): Promise<CartAddressDTO[]>
@@ -65,20 +74,32 @@ export interface ICartModuleService extends IModuleService {
deleteAddresses(ids: string[], sharedContext?: Context): Promise<void>
deleteAddresses(ids: string, sharedContext?: Context): Promise<void>
// addLineItems(data: AddLineItemsDTO, sharedContext?: Context): Promise<CartDTO>
// addLineItems(
// data: AddLineItemsDTO[],
// sharedContext?: Context
// ): Promise<CartDTO[]>
addLineItems(data: CreateLineItemForCartDTO): Promise<CartLineItemDTO>
addLineItems(data: CreateLineItemForCartDTO[]): Promise<CartLineItemDTO[]>
addLineItems(
cartId: string,
items: CreateLineItemDTO[],
sharedContext?: Context
): Promise<CartLineItemDTO[]>
// updateLineItems(
// data: UpdateLineItemsDTO,
// sharedContext?: Context
// ): Promise<CartDTO>
// updateLineItems(
// data: UpdateLineItemsDTO[],
// sharedContext?: Context
// ): Promise<CartDTO[]>
updateLineItems(
data: UpdateLineItemWithSelectorDTO[]
): Promise<CartLineItemDTO[]>
updateLineItems(
selector: Partial<CartLineItemDTO>,
data: Partial<UpdateLineItemDTO>,
sharedContext?: Context
): Promise<CartLineItemDTO[]>
updateLineItems(
lineId: string,
data: Partial<UpdateLineItemDTO>,
sharedContext?: Context
): Promise<CartLineItemDTO>
// removeLineItems(lineItemIds: string[], sharedContext?: Context): Promise<void>
removeLineItems(itemIds: string[], sharedContext?: Context): Promise<void>
removeLineItems(itemIds: string, sharedContext?: Context): Promise<void>
removeLineItems(
selector: Partial<CartLineItemDTO>,
sharedContext?: Context
): Promise<void>
}