feat(cart): Partial module service implementation (#6012)

Awaiting #6000 #6008  

**What**
- CRUD for Address in Cart Module service
- Tests for CRUD Carts + Address

**Not**
- Line items, shipping methods, tax lines, adjustment lines
This commit is contained in:
Oli Juhl
2024-01-12 11:30:57 +01:00
committed by GitHub
parent 8472460f53
commit 192bc336cc
16 changed files with 433 additions and 39 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/inventory": patch
"@medusajs/stock-location": patch
"@medusajs/types": patch
---
feat(cart): Partial module service implementation

View File

@@ -14,7 +14,7 @@ export const defaultCartsData = [
first_name: "Tony",
last_name: "Stark",
},
billing_address_id: {
billing_address: {
address_1: "Stark Industries",
city: "New York",
},

View File

@@ -68,7 +68,7 @@ describe("Address Service", () => {
.catch((e) => e)
expect(error.message).toContain(
"Address with id \"none-existing\" not found"
'Address with id "none-existing" not found'
)
})

View File

@@ -0,0 +1,245 @@
import { ICartModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initialize } from "../../../../src/initialize"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
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: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_CART_DB_SCHEMA,
},
})
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("create", () => {
it("should throw an error when required params are not passed", async () => {
const error = await service
.create([
{
email: "test@email.com",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for Cart.currency_code is required, 'undefined' found"
)
})
it("should create a cart successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [cart] = await service.list({ id: [createdCart.id] })
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
})
)
})
it("should create a cart with billing + shipping address successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
billing_address: {
first_name: "John",
last_name: "Doe",
},
shipping_address: {
first_name: "John",
last_name: "Doe",
},
},
])
const [cart] = await service.list(
{ id: [createdCart.id] },
{ relations: ["billing_address", "shipping_address"] }
)
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
billing_address: expect.objectContaining({
first_name: "John",
last_name: "Doe",
}),
shipping_address: expect.objectContaining({
first_name: "John",
last_name: "Doe",
}),
})
)
})
it("should create a cart with billing id + shipping id successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
last_name: "Doe",
},
])
const [createdCart] = await service.create([
{
currency_code: "eur",
billing_address_id: createdAddress.id,
shipping_address_id: createdAddress.id,
},
])
expect(createdCart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
billing_address: expect.objectContaining({
id: createdAddress.id,
first_name: "John",
last_name: "Doe",
}),
shipping_address: expect.objectContaining({
id: createdAddress.id,
first_name: "John",
last_name: "Doe",
}),
})
)
})
})
describe("update", () => {
it("should throw an error if cart does not exist", async () => {
const error = await service
.update([
{
id: "none-existing",
},
])
.catch((e) => e)
expect(error.message).toContain('Cart with id "none-existing" not found')
})
it("should update a cart successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [updatedCart] = await service.update([
{
id: createdCart.id,
email: "test@email.com",
},
])
const [cart] = await service.list({ id: [createdCart.id] })
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
email: updatedCart.email,
})
)
})
})
describe("delete", () => {
it("should delete a cart successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
await service.delete([createdCart.id])
const carts = await service.list({ id: [createdCart.id] })
expect(carts.length).toEqual(0)
})
})
describe("createAddresses", () => {
it("should create an address successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
},
])
const [address] = await service.listAddresses({
id: [createdAddress.id!],
})
expect(address).toEqual(
expect.objectContaining({
id: createdAddress.id,
first_name: "John",
})
)
})
})
describe("updateAddresses", () => {
it("should update an address successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
},
])
const [updatedAddress] = await service.updateAddresses([
{ id: createdAddress.id!, first_name: "Jane" },
])
expect(updatedAddress).toEqual(
expect.objectContaining({
id: createdAddress.id,
first_name: "Jane",
})
)
})
})
describe("deleteAddresses", () => {
it("should delete an address successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
},
])
await service.deleteAddresses([createdAddress.id!])
const [address] = await service.listAddresses({
id: [createdAddress.id!],
})
expect(address).toBe(undefined)
})
})
})

View File

@@ -17,7 +17,7 @@ describe("Cart Service", () => {
testManager = await MikroOrmWrapper.forkManager()
const cartRepository = new CartRepository({
manager: repositoryManager,
manager: repositoryManager
})
service = new CartService({
@@ -74,9 +74,7 @@ describe("Cart Service", () => {
])
.catch((e) => e)
expect(error.message).toContain(
"Cart with id \"none-existing\" not found"
)
expect(error.message).toContain('Cart with id "none-existing" not found')
})
it("should update a cart successfully", async () => {
@@ -87,7 +85,6 @@ describe("Cart Service", () => {
])
const [updatedCart] = await service.update([
{
id: createdCart.id,
email: "test@email.com",

View File

@@ -39,6 +39,7 @@
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
},
"devDependencies": {
"@medusajs/types": "workspace:^",
"@mikro-orm/cli": "5.9.7",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
@@ -51,7 +52,6 @@
},
"dependencies": {
"@medusajs/modules-sdk": "^1.12.5",
"@medusajs/types": "^1.11.9",
"@medusajs/utils": "^1.11.2",
"@mikro-orm/core": "5.9.7",
"@mikro-orm/migrations": "5.9.7",

View File

@@ -10,7 +10,11 @@ import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types"
export const initialize = async (
options?: ModulesSdkTypes.ModuleBootstrapDeclaration,
options?:
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
| ExternalModuleDeclaration
| InternalModuleDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<ICartModuleService> => {
const loaded = await MedusaModule.bootstrap<ICartModuleService>({

View File

@@ -3,6 +3,7 @@ import * as defaultRepositories from "@repositories"
import { LoaderOptions } from "@medusajs/modules-sdk"
import { ModulesSdkTypes } from "@medusajs/types"
import { loadCustomRepositories } from "@medusajs/utils"
import * as defaultServices from "@services"
import { asClass } from "awilix"
export default async ({
@@ -17,7 +18,8 @@ export default async ({
)?.repositories
container.register({
// cartService: asClass(defaultServices.CartService).singleton(),
cartService: asClass(defaultServices.CartService).singleton(),
addressService: asClass(defaultServices.AddressService).singleton(),
})
if (customRepositories) {
@@ -34,5 +36,7 @@ export default async ({
function loadDefaultRepositories({ container }) {
container.register({
baseRepository: asClass(defaultRepositories.BaseRepository).singleton(),
cartRepository: asClass(defaultRepositories.CartRepository).singleton(),
addressRepository: asClass(defaultRepositories.AddressRepository).singleton(),
})
}

View File

@@ -5,9 +5,10 @@ import {
Cascade,
Collection,
Entity,
Index,
ManyToOne,
OnInit,
OneToMany,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
@@ -19,7 +20,7 @@ import ShippingMethod from "./shipping-method"
type OptionalCartProps =
| "shipping_address"
| "billing_address"
| DAL.EntityDateColumns // TODO: To be revisited when more clear
| DAL.EntityDateColumns
@Entity({ tableName: "cart" })
export default class Cart {
@@ -47,18 +48,22 @@ export default class Cart {
@Property({ columnType: "text" })
currency_code: string
@OneToOne({
entity: () => Address,
joinColumn: "shipping_address_id",
cascade: [Cascade.REMOVE],
@Index({ name: "IDX_cart_shipping_address_id" })
@Property({ columnType: "text", nullable: true })
shipping_address_id?: string | null
@ManyToOne(() => Address, {
fieldName: "shipping_address_id",
nullable: true,
})
shipping_address?: Address | null
@OneToOne({
entity: () => Address,
joinColumn: "billing_address_id",
cascade: [Cascade.REMOVE],
@Index({ name: "IDX_cart_billing_address_id" })
@Property({ columnType: "text", nullable: true })
billing_address_id?: string | null
@ManyToOne(() => Address, {
fieldName: "billing_address_id",
nullable: true,
})
billing_address?: Address | null
@@ -116,7 +121,7 @@ export default class Cart {
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
created_at?: Date
@Property({
onCreate: () => new Date(),
@@ -124,7 +129,10 @@ export default class Cart {
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
updated_at?: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@BeforeCreate()
onCreate() {

View File

@@ -6,3 +6,4 @@ 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"
export { default as ShippingMethodTaxLine } from "./shipping-method-tax-line"

View File

@@ -1,4 +1,6 @@
import {
AddressDTO,
CartAddressDTO,
CartDTO,
Context,
CreateCartDTO,
@@ -11,11 +13,13 @@ import {
UpdateCartDTO,
} from "@medusajs/types"
import { FilterableAddressProps } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/utils"
import { CreateAddressDTO, UpdateAddressDTO } from "@types"
import { joinerConfig } from "../joiner-config"
import AddressService from "./address"
import CartService from "./cart"
@@ -32,10 +36,12 @@ export default class CartModuleService implements ICartModuleService {
protected addressService_: AddressService
constructor(
{ baseRepository }: InjectedDependencies,
{ baseRepository, cartService, addressService }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.cartService_ = cartService
this.addressService_ = addressService
}
__joinerConfig(): ModuleJoinerConfig {
@@ -157,15 +163,9 @@ export default class CartModuleService implements ICartModuleService {
return await this.cartService_.update(data, sharedContext)
}
async delete(
ids: string[],
sharedContext?: Context
): Promise<void>
async delete(ids: string[], sharedContext?: Context): Promise<void>
async delete(
ids: string,
sharedContext?: Context
): Promise<void>
async delete(ids: string, sharedContext?: Context): Promise<void>
@InjectTransactionManager("baseRepository_")
async delete(
@@ -175,4 +175,93 @@ export default class CartModuleService implements ICartModuleService {
const cartIds = Array.isArray(ids) ? ids : [ids]
await this.cartService_.delete(cartIds, sharedContext)
}
@InjectManager("baseRepository_")
async listAddresses(
filters: FilterableAddressProps = {},
config: FindConfig<AddressDTO> = {},
@MedusaContext() sharedContext: Context = {}
) {
const addresses = await this.addressService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<CartAddressDTO[]>(addresses, {
populate: true,
})
}
async createAddresses(data: CreateAddressDTO, sharedContext?: Context)
async createAddresses(data: CreateAddressDTO[], sharedContext?: Context)
@InjectManager("baseRepository_")
async createAddresses(
data: CreateAddressDTO[] | CreateAddressDTO,
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const addresses = await this.createAddresses_(input, sharedContext)
const result = await this.listAddresses(
{ id: addresses.map((p) => p.id) },
{},
sharedContext
)
return (Array.isArray(data) ? result : result[0]) as
| AddressDTO
| AddressDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async createAddresses_(
data: CreateAddressDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.addressService_.create(data, sharedContext)
}
async updateAddresses(data: UpdateAddressDTO, sharedContext?: Context)
async updateAddresses(data: UpdateAddressDTO[], sharedContext?: Context)
@InjectManager("baseRepository_")
async updateAddresses(
data: UpdateAddressDTO[] | UpdateAddressDTO,
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const addresses = await this.updateAddresses_(input, sharedContext)
const result = await this.listAddresses(
{ id: addresses.map((p) => p.id) },
{},
sharedContext
)
return (Array.isArray(data) ? result : result[0]) as
| AddressDTO
| AddressDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async updateAddresses_(
data: UpdateAddressDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.addressService_.update(data, sharedContext)
}
async deleteAddresses(ids: string[], sharedContext?: Context)
async deleteAddresses(ids: string, sharedContext?: Context)
@InjectTransactionManager("baseRepository_")
async deleteAddresses(
ids: string[] | string,
@MedusaContext() sharedContext: Context = {}
) {
const addressIds = Array.isArray(ids) ? ids : [ids]
await this.addressService_.delete(addressIds, sharedContext)
}
}

View File

@@ -20,7 +20,7 @@
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/types": "^1.11.8",
"@medusajs/types": "^1.11.9",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"rimraf": "^5.0.1",

View File

@@ -20,7 +20,7 @@
"author": "Medusa",
"license": "MIT",
"devDependencies": {
"@medusajs/types": "^1.11.8",
"@medusajs/types": "^1.11.9",
"cross-env": "^5.2.1",
"jest": "^29.6.3",
"rimraf": "^5.0.1",

View File

@@ -27,6 +27,9 @@ export interface CreateCartDTO {
email?: string
currency_code: string
shipping_address_id?: string
billing_address_id?: string
shipping_address?: CreateAddressDTO | UpdateAddressDTO
billing_address?: CreateAddressDTO | UpdateAddressDTO
@@ -42,6 +45,9 @@ export interface UpdateCartDTO {
email?: string
currency_code?: string
shipping_address_id?: string
billing_address_id?: string
billing_address?: CreateAddressDTO | UpdateAddressDTO
shipping_address?: CreateAddressDTO | UpdateAddressDTO

View File

@@ -1,8 +1,14 @@
import { AddressDTO } from "../address"
import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import { CartDTO, FilterableCartProps } from "./common"
import { CreateCartDTO, UpdateCartDTO } from "./mutations"
import { CartAddressDTO, CartDTO, FilterableAddressProps, FilterableCartProps } from "./common"
import {
CreateAddressDTO,
CreateCartDTO,
UpdateAddressDTO,
UpdateCartDTO,
} from "./mutations"
export interface ICartModuleService extends IModuleService {
retrieve(
@@ -32,6 +38,33 @@ export interface ICartModuleService extends IModuleService {
delete(cartIds: string[], sharedContext?: Context): Promise<void>
delete(cartId: string, sharedContext?: Context): Promise<void>
listAddresses(
filters?: FilterableAddressProps,
config?: FindConfig<AddressDTO>,
sharedContext?: Context
): Promise<CartAddressDTO[]>
createAddresses(
data: CreateAddressDTO[],
sharedContext?: Context
): Promise<CartAddressDTO[]>
createAddresses(
data: CreateAddressDTO,
sharedContext?: Context
): Promise<CartAddressDTO>
updateAddresses(
data: UpdateAddressDTO[],
sharedContext?: Context
): Promise<CartAddressDTO[]>
updateAddresses(
data: UpdateAddressDTO,
sharedContext?: Context
): Promise<CartAddressDTO>
deleteAddresses(ids: string[], sharedContext?: Context): Promise<void>
deleteAddresses(ids: string, sharedContext?: Context): Promise<void>
// addLineItems(data: AddLineItemsDTO, sharedContext?: Context): Promise<CartDTO>
// addLineItems(
// data: AddLineItemsDTO[],

View File

@@ -7888,7 +7888,7 @@ __metadata:
resolution: "@medusajs/cart@workspace:packages/cart"
dependencies:
"@medusajs/modules-sdk": ^1.12.5
"@medusajs/types": ^1.11.9
"@medusajs/types": "workspace:^"
"@medusajs/utils": ^1.11.2
"@mikro-orm/cli": 5.9.7
"@mikro-orm/core": 5.9.7
@@ -8072,7 +8072,7 @@ __metadata:
resolution: "@medusajs/inventory@workspace:packages/inventory"
dependencies:
"@medusajs/modules-sdk": ^1.12.4
"@medusajs/types": ^1.11.8
"@medusajs/types": ^1.11.9
"@medusajs/utils": ^1.11.1
awilix: ^8.0.0
cross-env: ^5.2.1
@@ -8478,7 +8478,7 @@ __metadata:
resolution: "@medusajs/stock-location@workspace:packages/stock-location"
dependencies:
"@medusajs/modules-sdk": ^1.12.4
"@medusajs/types": ^1.11.8
"@medusajs/types": ^1.11.9
"@medusajs/utils": ^1.11.1
awilix: ^8.0.0
cross-env: ^5.2.1
@@ -8516,7 +8516,7 @@ __metadata:
languageName: unknown
linkType: soft
"@medusajs/types@^1.10.0, @medusajs/types@^1.11.10, @medusajs/types@^1.11.5, @medusajs/types@^1.11.6, @medusajs/types@^1.11.8, @medusajs/types@^1.11.9, @medusajs/types@^1.8.10, @medusajs/types@workspace:packages/types":
"@medusajs/types@^1.10.0, @medusajs/types@^1.11.10, @medusajs/types@^1.11.5, @medusajs/types@^1.11.6, @medusajs/types@^1.11.9, @medusajs/types@^1.8.10, @medusajs/types@workspace:^, @medusajs/types@workspace:packages/types":
version: 0.0.0-use.local
resolution: "@medusajs/types@workspace:packages/types"
dependencies: