diff --git a/.changeset/soft-papayas-switch.md b/.changeset/soft-papayas-switch.md new file mode 100644 index 0000000000..e822f37ec4 --- /dev/null +++ b/.changeset/soft-papayas-switch.md @@ -0,0 +1,7 @@ +--- +"@medusajs/inventory": patch +"@medusajs/stock-location": patch +"@medusajs/types": patch +--- + +feat(cart): Partial module service implementation diff --git a/packages/cart/integration-tests/__fixtures__/cart/data.ts b/packages/cart/integration-tests/__fixtures__/cart/data.ts index ba3eb43849..dd9169b24d 100644 --- a/packages/cart/integration-tests/__fixtures__/cart/data.ts +++ b/packages/cart/integration-tests/__fixtures__/cart/data.ts @@ -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", }, diff --git a/packages/cart/integration-tests/__tests__/services/address/index.spec.ts b/packages/cart/integration-tests/__tests__/services/address/index.spec.ts index 54b1395499..4ec32c1284 100644 --- a/packages/cart/integration-tests/__tests__/services/address/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/address/index.spec.ts @@ -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' ) }) 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 new file mode 100644 index 0000000000..151343e1d1 --- /dev/null +++ b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -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) + }) + }) +}) diff --git a/packages/cart/integration-tests/__tests__/services/cart/index.spec.ts b/packages/cart/integration-tests/__tests__/services/cart/index.spec.ts index b1dfeb1826..b5eb017f0a 100644 --- a/packages/cart/integration-tests/__tests__/services/cart/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/cart/index.spec.ts @@ -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", diff --git a/packages/cart/package.json b/packages/cart/package.json index c84323c977..46e9908a95 100644 --- a/packages/cart/package.json +++ b/packages/cart/package.json @@ -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", diff --git a/packages/cart/src/initialize/index.ts b/packages/cart/src/initialize/index.ts index bd54c0e101..4774d6c207 100644 --- a/packages/cart/src/initialize/index.ts +++ b/packages/cart/src/initialize/index.ts @@ -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 => { const loaded = await MedusaModule.bootstrap({ diff --git a/packages/cart/src/loaders/container.ts b/packages/cart/src/loaders/container.ts index 7d56fa3f22..a8716ea5ab 100644 --- a/packages/cart/src/loaders/container.ts +++ b/packages/cart/src/loaders/container.ts @@ -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(), }) } diff --git a/packages/cart/src/models/cart.ts b/packages/cart/src/models/cart.ts index 9526e3054e..2dfc02aab4 100644 --- a/packages/cart/src/models/cart.ts +++ b/packages/cart/src/models/cart.ts @@ -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() { diff --git a/packages/cart/src/models/index.ts b/packages/cart/src/models/index.ts index 1c9016ae7f..9451e322dc 100644 --- a/packages/cart/src/models/index.ts +++ b/packages/cart/src/models/index.ts @@ -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" + diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index b951b660b7..419c71b84f 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -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 + async delete(ids: string[], sharedContext?: Context): Promise - async delete( - ids: string, - sharedContext?: Context - ): Promise + async delete(ids: string, sharedContext?: Context): Promise @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 = {}, + @MedusaContext() sharedContext: Context = {} + ) { + const addresses = await this.addressService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize(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) + } } diff --git a/packages/inventory/package.json b/packages/inventory/package.json index 9ee5e93437..13a86a8f4e 100644 --- a/packages/inventory/package.json +++ b/packages/inventory/package.json @@ -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", diff --git a/packages/stock-location/package.json b/packages/stock-location/package.json index be792ada5f..ff2eea5921 100644 --- a/packages/stock-location/package.json +++ b/packages/stock-location/package.json @@ -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", diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index 4fd39b5274..0ed7f581aa 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -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 diff --git a/packages/types/src/cart/service.ts b/packages/types/src/cart/service.ts index 800ede55a3..caba8cfb1d 100644 --- a/packages/types/src/cart/service.ts +++ b/packages/types/src/cart/service.ts @@ -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 delete(cartId: string, sharedContext?: Context): Promise + listAddresses( + filters?: FilterableAddressProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + createAddresses( + data: CreateAddressDTO[], + sharedContext?: Context + ): Promise + createAddresses( + data: CreateAddressDTO, + sharedContext?: Context + ): Promise + + updateAddresses( + data: UpdateAddressDTO[], + sharedContext?: Context + ): Promise + updateAddresses( + data: UpdateAddressDTO, + sharedContext?: Context + ): Promise + + deleteAddresses(ids: string[], sharedContext?: Context): Promise + deleteAddresses(ids: string, sharedContext?: Context): Promise + // addLineItems(data: AddLineItemsDTO, sharedContext?: Context): Promise // addLineItems( // data: AddLineItemsDTO[], diff --git a/yarn.lock b/yarn.lock index 3edcaca246..1850f0f6e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: