From 9fda6a68240268e756aa5e7dca001a1f1744811d Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Fri, 2 Feb 2024 18:45:32 +0800 Subject: [PATCH 1/4] feat(auth): add authentication endpoints (#6265) **What** - Add authentication endpoints: - `/auth/[scope]/[provider]` - `/auth/[scope]/[provider]/callback` - update authenticate-middleware handler - Add scope field to user - Add unique constraint on scope and entity_id note: there's still some remaining work related to jwt auth to be handled, this is mainly focussed on session auth with endpoints Co-authored-by: Sebastian Rindom <7554214+srindom@users.noreply.github.com> --- .../customer/store/create-customer.spec.ts | 10 ++-- .../helpers/create-authenticated-customer.ts | 1 + .../__fixtures__/auth-user/index.ts | 5 +- .../services/auth-user/index.spec.ts | 10 ++-- .../services/module/auth-user.spec.ts | 1 + .../services/module/providers.spec.ts | 2 +- .../providers/username-password.spec.ts | 20 +++---- packages/auth/package.json | 2 +- ...cation.json => .snapshot-medusa-auth.json} | 28 +++++----- .../src/migrations/Migration20240122041959.ts | 30 ----------- .../src/migrations/Migration20240201100135.ts | 22 ++++++++ packages/auth/src/models/auth-provider.ts | 4 +- packages/auth/src/models/auth-user.ts | 14 +++-- packages/auth/src/providers/email-password.ts | 54 ++++++++++++++++--- packages/auth/src/providers/google.ts | 38 ++++++------- packages/auth/src/services/auth-module.ts | 4 +- .../auth/src/types/services/auth-provider.ts | 11 +--- packages/auth/src/types/services/auth-user.ts | 4 +- packages/customer/package.json | 2 +- .../[scope]/[authProvider]/callback/route.ts | 46 ++++++++++++++++ .../auth/[scope]/[authProvider]/route.ts | 46 ++++++++++++++++ .../src/api-v2/store/customers/me/route.ts | 5 +- .../src/api-v2/store/customers/middlewares.ts | 3 +- .../api-v2/store/customers/query-config.ts | 2 +- .../src/api-v2/store/customers/route.ts | 28 +++++++++- packages/medusa/src/types/routing.ts | 4 +- .../src/utils/authenticate-middleware.ts | 23 ++++---- .../types/src/auth/common/auth-provider.ts | 15 ++---- packages/types/src/auth/common/auth-user.ts | 6 ++- packages/types/src/auth/common/provider.ts | 6 +-- yarn.lock | 4 +- 31 files changed, 303 insertions(+), 147 deletions(-) rename packages/auth/src/migrations/{.snapshot-medusa-authentication.json => .snapshot-medusa-auth.json} (91%) delete mode 100644 packages/auth/src/migrations/Migration20240122041959.ts create mode 100644 packages/auth/src/migrations/Migration20240201100135.ts create mode 100644 packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts create mode 100644 packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts diff --git a/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts b/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts index 6b986b0bc04b6..bae4ee4918c8d 100644 --- a/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts +++ b/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts @@ -1,11 +1,12 @@ +import { IAuthModuleService, ICustomerModuleService } from "@medusajs/types" +import { initDb, useDb } from "../../../../environment-helpers/use-db" + import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { ICustomerModuleService, IAuthModuleService } from "@medusajs/types" +import adminSeeder from "../../../../helpers/admin-seeder" +import { getContainer } from "../../../../environment-helpers/use-container" import path from "path" import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import { useApi } from "../../../../environment-helpers/use-api" -import { getContainer } from "../../../../environment-helpers/use-container" -import { initDb, useDb } from "../../../../environment-helpers/use-db" -import adminSeeder from "../../../../helpers/admin-seeder" jest.setTimeout(50000) @@ -49,6 +50,7 @@ describe("POST /store/customers", () => { const authUser = await authService.createAuthUser({ entity_id: "store_user", provider_id: "test", + scope: "store", }) const jwt = await authService.generateJwtToken(authUser.id, "store") diff --git a/integration-tests/plugins/helpers/create-authenticated-customer.ts b/integration-tests/plugins/helpers/create-authenticated-customer.ts index 54f19cf0973c6..3bb092d950d2a 100644 --- a/integration-tests/plugins/helpers/create-authenticated-customer.ts +++ b/integration-tests/plugins/helpers/create-authenticated-customer.ts @@ -13,6 +13,7 @@ export const createAuthenticatedCustomer = async ( const authUser = await authService.createAuthUser({ entity_id: "store_user", provider_id: "test", + scope: "store", app_metadata: { customer_id: customer.id }, }) diff --git a/packages/auth/integration-tests/__fixtures__/auth-user/index.ts b/packages/auth/integration-tests/__fixtures__/auth-user/index.ts index 46e745ffdc18b..fefbdc9a6d90c 100644 --- a/packages/auth/integration-tests/__fixtures__/auth-user/index.ts +++ b/packages/auth/integration-tests/__fixtures__/auth-user/index.ts @@ -1,5 +1,5 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" import { AuthUser } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" export async function createAuthUsers( manager: SqlEntityManager, @@ -8,15 +8,18 @@ export async function createAuthUsers( id: "test-id", entity_id: "test-id", provider: "manual", + scope: "store", }, { id: "test-id-1", entity_id: "test-id-1", provider: "manual", + scope: "store", }, { entity_id: "test-id-2", provider: "store", + scope: "store", }, ] ): Promise { diff --git a/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts b/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts index 07f7aa2426d9e..1737aa19b4f97 100644 --- a/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts @@ -1,12 +1,11 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" import { AuthUserService } from "@services" - +import ContainerLoader from "../../../../src/loaders/container" import { MikroOrmWrapper } from "../../../utils" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { asValue } from "awilix" import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthUsers } from "../../../__fixtures__/auth-user" import { createMedusaContainer } from "@medusajs/utils" -import { asValue } from "awilix" -import ContainerLoader from "../../../../src/loaders/container" jest.setTimeout(30000) @@ -229,7 +228,8 @@ describe("AuthUser Service", () => { { id: "test", provider_id: "manual", - entity_id: "test" + entity_id: "test", + scope: "store" }, ]) diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts index 86fde3c04bab6..641ee17e1dd33 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts @@ -246,6 +246,7 @@ describe("AuthModuleService - AuthUser", () => { id: "test", provider_id: "manual", entity_id: "test", + scope: "store", }, ]) diff --git a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts index 556db598380d5..f3b79046f4d66 100644 --- a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts @@ -83,7 +83,7 @@ describe("AuthModuleService - AuthProvider", () => { const { success, error } = await service.authenticate( "emailpass", { - scope: "non-existing", + authScope: "non-existing", } as any ) diff --git a/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts index 3cd76abcd9408..7e3955d71b9b5 100644 --- a/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -1,6 +1,6 @@ -import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" import { MedusaModule, Modules } from "@medusajs/modules-sdk" +import { IAuthModuleService } from "@medusajs/types" import { MikroOrmWrapper } from "../../../utils" import Scrypt from "scrypt-kdf" import { SqlEntityManager } from "@mikro-orm/postgresql" @@ -62,6 +62,7 @@ describe("AuthModuleService - AuthProvider", () => { { provider: "emailpass", entity_id: email, + scope: "store", provider_metadata: { password: passwordHash, }, @@ -73,8 +74,8 @@ describe("AuthModuleService - AuthProvider", () => { email: "test@test.com", password: password, }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: true, @@ -92,8 +93,8 @@ describe("AuthModuleService - AuthProvider", () => { const res = await service.authenticate("emailpass", { body: { email: "test@test.com" }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: false, @@ -106,8 +107,8 @@ describe("AuthModuleService - AuthProvider", () => { const res = await service.authenticate("emailpass", { body: { password: "supersecret" }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: false, @@ -127,6 +128,7 @@ describe("AuthModuleService - AuthProvider", () => { // Add authenticated user { provider: "emailpass", + scope: "store", entity_id: email, provider_metadata: { password_hash: passwordHash, @@ -139,8 +141,8 @@ describe("AuthModuleService - AuthProvider", () => { email: "test@test.com", password: "password", }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: false, diff --git a/packages/auth/package.json b/packages/auth/package.json index ac68f58918135..4516db7edd6ae 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.3.1", "jsonwebtoken": "^9.0.2", "knex": "2.4.2", "scrypt-kdf": "^2.0.1", diff --git a/packages/auth/src/migrations/.snapshot-medusa-authentication.json b/packages/auth/src/migrations/.snapshot-medusa-auth.json similarity index 91% rename from packages/auth/src/migrations/.snapshot-medusa-authentication.json rename to packages/auth/src/migrations/.snapshot-medusa-auth.json index a31fe796e7234..294b0a9ba5d4b 100644 --- a/packages/auth/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/auth/src/migrations/.snapshot-medusa-auth.json @@ -24,20 +24,14 @@ "nullable": false, "mappedType": "text" }, - "domain": { - "name": "domain", + "scope": { + "name": "scope", "type": "text", "unsigned": false, "autoincrement": false, "primary": false, - "nullable": false, - "default": "'all'", - "enumItems": [ - "all", - "store", - "admin" - ], - "mappedType": "enum" + "nullable": true, + "mappedType": "text" }, "config": { "name": "config", @@ -104,6 +98,15 @@ "nullable": true, "mappedType": "text" }, + "scope": { + "name": "scope", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "user_metadata": { "name": "user_metadata", "type": "jsonb", @@ -119,7 +122,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "json" }, "provider_metadata": { @@ -136,9 +139,10 @@ "schema": "public", "indexes": [ { - "keyName": "IDX_auth_user_provider_entity_id", + "keyName": "IDX_auth_user_provider_scope_entity_id", "columnNames": [ "provider_id", + "scope", "entity_id" ], "composite": true, diff --git a/packages/auth/src/migrations/Migration20240122041959.ts b/packages/auth/src/migrations/Migration20240122041959.ts deleted file mode 100644 index 15f1526572d4f..0000000000000 --- a/packages/auth/src/migrations/Migration20240122041959.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Migration } from "@mikro-orm/migrations" - -export class Migration20240122041959 extends Migration { - async up(): Promise { - this.addSql( - 'create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));' - ) - - this.addSql( - 'create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));' - ) - this.addSql( - 'alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");' - ) - - this.addSql( - 'alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;' - ) - } - - async down(): Promise { - this.addSql( - 'alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";' - ) - - this.addSql('drop table if exists "auth_provider" cascade;') - - this.addSql('drop table if exists "auth_user" cascade;') - } -} diff --git a/packages/auth/src/migrations/Migration20240201100135.ts b/packages/auth/src/migrations/Migration20240201100135.ts new file mode 100644 index 0000000000000..de100203d9a04 --- /dev/null +++ b/packages/auth/src/migrations/Migration20240201100135.ts @@ -0,0 +1,22 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240201100135 extends Migration { + + async up(): Promise { + this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "scope" text null, "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + + this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "scope" text not null, "user_metadata" jsonb null, "app_metadata" jsonb not null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_scope_entity_id" unique ("provider_id", "scope", "entity_id");'); + + this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); + } + + async down(): Promise { + this.addSql('alter table "auth_user" drop constraint "auth_user_provider_id_foreign";'); + + this.addSql('drop table if exists "auth_provider" cascade;'); + + this.addSql('drop table if exists "auth_user" cascade;'); + } + +} diff --git a/packages/auth/src/models/auth-provider.ts b/packages/auth/src/models/auth-provider.ts index 0827186069bbd..39ba51823585d 100644 --- a/packages/auth/src/models/auth-provider.ts +++ b/packages/auth/src/models/auth-provider.ts @@ -20,8 +20,8 @@ export default class AuthProvider { @Property({ columnType: "text" }) name: string - @Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL }) - domain: ProviderDomain = ProviderDomain.ALL + @Property({ columnType: "text", nullable: true }) + scope: string @Property({ columnType: "jsonb", nullable: true }) config: Record | null = null diff --git a/packages/auth/src/models/auth-user.ts b/packages/auth/src/models/auth-user.ts index 0c10053175fae..122462c26cdf1 100644 --- a/packages/auth/src/models/auth-user.ts +++ b/packages/auth/src/models/auth-user.ts @@ -17,7 +17,10 @@ import { generateEntityId } from "@medusajs/utils" type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata" @Entity() -@Unique({ properties: ["provider","entity_id" ], name: "IDX_auth_user_provider_entity_id" }) +@Unique({ + properties: ["provider", "scope", "entity_id"], + name: "IDX_auth_user_provider_scope_entity_id", +}) export default class AuthUser { [OptionalProps]: OptionalFields @@ -34,14 +37,17 @@ export default class AuthUser { }) provider: AuthProvider + @Property({ columnType: "text" }) + scope: string + @Property({ columnType: "jsonb", nullable: true }) user_metadata: Record | null - @Property({ columnType: "jsonb", nullable: true }) - app_metadata: Record | null + @Property({ columnType: "jsonb" }) + app_metadata: Record = {} @Property({ columnType: "jsonb", nullable: true }) - provider_metadata: Record | null + provider_metadata: Record | null = null @BeforeCreate() onCreate() { diff --git a/packages/auth/src/providers/email-password.ts b/packages/auth/src/providers/email-password.ts index 3e9760f991041..da2617ef2a81f 100644 --- a/packages/auth/src/providers/email-password.ts +++ b/packages/auth/src/providers/email-password.ts @@ -1,4 +1,8 @@ -import { AbstractAuthModuleProvider, isString } from "@medusajs/utils" +import { + AbstractAuthModuleProvider, + MedusaError, + isString, +} from "@medusajs/utils" import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AuthUserService } from "@services" @@ -16,6 +20,17 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider { this.authUserSerivce_ = authUserService } + private getHashConfig(scope: string) { + const scopeConfig = this.scopes_[scope].hashConfig as + | Scrypt.ScryptParams + | undefined + + const defaultHashConfig = { logN: 15, r: 8, p: 1 } + + // Return custom defined hash config or default hash parameters + return scopeConfig ?? defaultHashConfig + } + async authenticate( userData: AuthenticationInput ): Promise { @@ -34,11 +49,38 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider { error: "Email should be a string", } } - - const authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - email, - EmailPasswordProvider.PROVIDER - ) + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + email, + EmailPasswordProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + const password_hash = await Scrypt.kdf( + password, + this.getHashConfig(userData.authScope) + ) + + const [createdAuthUser] = await this.authUserSerivce_.create([ + { + entity_id: email, + provider: EmailPasswordProvider.PROVIDER, + scope: userData.authScope, + provider_metadata: { + password: password_hash.toString("base64"), + }, + }, + ]) + + return { + success: true, + authUser: JSON.parse(JSON.stringify(createdAuthUser)), + } + } + return { success: false, error: error.message } + } const password_hash = authUser.provider_metadata?.password diff --git a/packages/auth/src/providers/google.ts b/packages/auth/src/providers/google.ts index 5a94eb992260f..b5558216e1240 100644 --- a/packages/auth/src/providers/google.ts +++ b/packages/auth/src/providers/google.ts @@ -1,7 +1,4 @@ -import { - AbstractAuthModuleProvider, - MedusaError, -} from "@medusajs/utils" +import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils" import { AuthProviderScope, AuthenticationInput, @@ -9,6 +6,7 @@ import { } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" + import { AuthorizationCode } from "simple-oauth2" import url from "url" @@ -78,7 +76,7 @@ class GoogleProvider extends AbstractAuthModuleProvider { const code = req.query?.code ?? req.body?.code - return await this.validateCallbackToken(code, req.scope, config) + return await this.validateCallbackToken(code, req.authScope, config) } // abstractable @@ -97,14 +95,15 @@ class GoogleProvider extends AbstractAuthModuleProvider { ) } catch (error) { if (error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([ + const [createdAuthUser] = await this.authUserSerivce_.create([ { entity_id, - provider_id: GoogleProvider.PROVIDER, + provider: GoogleProvider.PROVIDER, user_metadata: jwtData!.payload, - app_metadata: { scope }, + scope, }, ]) + authUser = createdAuthUser } else { return { success: false, error: error.message } } @@ -135,24 +134,20 @@ class GoogleProvider extends AbstractAuthModuleProvider { } } - private getConfigFromScope(config: AuthProviderScope): ProviderConfig { - const providerConfig: Partial = {} + private getConfigFromScope( + config: AuthProviderScope & Partial + ): ProviderConfig { + const providerConfig: Partial = { ...config } - if (config.clientId) { - providerConfig.clientID = config.clientId - } else { + if (!providerConfig.clientID) { throw new Error("Google clientID is required") } - if (config.clientSecret) { - providerConfig.clientSecret = config.clientSecret - } else { + if (!providerConfig.clientSecret) { throw new Error("Google clientSecret is required") } - if (config.callbackURL) { - providerConfig.callbackURL = config.callbackUrl - } else { + if (!providerConfig.callbackURL) { throw new Error("Google callbackUrl is required") } @@ -160,9 +155,8 @@ class GoogleProvider extends AbstractAuthModuleProvider { } private originalURL(req: AuthenticationInput) { - const tls = req.connection.encrypted const host = req.headers.host - const protocol = tls ? "https" : "http" + const protocol = req.protocol const path = req.url || "" return protocol + "://" + host + path @@ -173,7 +167,7 @@ class GoogleProvider extends AbstractAuthModuleProvider { ): Promise { await this.authProviderService_.retrieve(GoogleProvider.PROVIDER) - const scopeConfig = this.scopes_[req.scope] + const scopeConfig = this.scopes_[req.authScope] const config = this.getConfigFromScope(scopeConfig) diff --git a/packages/auth/src/services/auth-module.ts b/packages/auth/src/services/auth-module.ts index 3310c28c1f407..34ac05f0dda7d 100644 --- a/packages/auth/src/services/auth-module.ts +++ b/packages/auth/src/services/auth-module.ts @@ -395,7 +395,7 @@ export default class AuthModuleService< protected getRegisteredAuthenticationProvider( provider: string, - { scope }: AuthenticationInput + { authScope }: AuthenticationInput ): AbstractAuthModuleProvider { let containerProvider: AbstractAuthModuleProvider try { @@ -407,7 +407,7 @@ export default class AuthModuleService< ) } - containerProvider.validateScope(scope) + containerProvider.validateScope(authScope) return containerProvider } diff --git a/packages/auth/src/types/services/auth-provider.ts b/packages/auth/src/types/services/auth-provider.ts index 8ef5d9b3b9bdd..dc400222e43a1 100644 --- a/packages/auth/src/types/services/auth-provider.ts +++ b/packages/auth/src/types/services/auth-provider.ts @@ -1,7 +1,7 @@ export type AuthProviderDTO = { provider: string name: string - domain: ProviderDomain + scope: string is_active: boolean config: Record } @@ -9,7 +9,7 @@ export type AuthProviderDTO = { export type CreateAuthProviderDTO = { provider: string name: string - domain?: ProviderDomain + scope?: string is_active?: boolean config?: Record } @@ -17,15 +17,8 @@ export type CreateAuthProviderDTO = { export type UpdateAuthProviderDTO = { provider: string name?: string - domain?: ProviderDomain is_active?: boolean config?: Record } -export enum ProviderDomain { - ALL = "all", - STORE = "store", - ADMIN = "admin", -} - export type FilterableAuthProviderProps = {} diff --git a/packages/auth/src/types/services/auth-user.ts b/packages/auth/src/types/services/auth-user.ts index c059e980f8b9c..bab3e0c2e4f9d 100644 --- a/packages/auth/src/types/services/auth-user.ts +++ b/packages/auth/src/types/services/auth-user.ts @@ -4,6 +4,7 @@ export type AuthUserDTO = { id: string provider_id: string entity_id: string + scope: string provider: AuthProviderDTO provider_metadata?: Record user_metadata: Record @@ -12,7 +13,8 @@ export type AuthUserDTO = { export type CreateAuthUserDTO = { entity_id: string - provider_id: string + provider: string + scope: string provider_metadata?: Record user_metadata?: Record app_metadata?: Record diff --git a/packages/customer/package.json b/packages/customer/package.json index 6f58a6cf6c197..637c791163066 100644 --- a/packages/customer/package.json +++ b/packages/customer/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.3.1", "knex": "2.4.2" } } diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts new file mode 100644 index 0000000000000..54066dce4f3fd --- /dev/null +++ b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts @@ -0,0 +1,46 @@ +import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" + +import { MedusaError } from "@medusajs/utils" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const { scope, authProvider } = req.params + + const service: IAuthModuleService = req.scope.resolve( + ModuleRegistrationName.AUTH + ) + + const authData = { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: scope, + protocol: req.protocol, + } as AuthenticationInput + + const authResult = await service.validateCallback(authProvider, authData) + + const { success, error, authUser, location } = authResult + if (location) { + res.redirect(location) + return + } + + if (success) { + req.session.auth_user = authUser + req.session.scope = authUser.scope + + return res.status(200).json({ authUser }) + } + + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Authentication failed" + ) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + await GET(req, res) +} diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts new file mode 100644 index 0000000000000..7d3873a320ccd --- /dev/null +++ b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts @@ -0,0 +1,46 @@ +import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" + +import { MedusaError } from "@medusajs/utils" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const { scope, authProvider } = req.params + + const service: IAuthModuleService = req.scope.resolve( + ModuleRegistrationName.AUTH + ) + + const authData = { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: scope, + protocol: req.protocol, + } as AuthenticationInput + + const authResult = await service.authenticate(authProvider, authData) + + const { success, error, authUser, location } = authResult + if (location) { + res.redirect(location) + return + } + + if (success) { + req.session.auth_user = authUser + req.session.scope = authUser.scope + + return res.status(200).json({ authUser }) + } + + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Authentication failed" + ) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + await GET(req, res) +} diff --git a/packages/medusa/src/api-v2/store/customers/me/route.ts b/packages/medusa/src/api-v2/store/customers/me/route.ts index 22a9746d97428..83b654f84cbb5 100644 --- a/packages/medusa/src/api-v2/store/customers/me/route.ts +++ b/packages/medusa/src/api-v2/store/customers/me/route.ts @@ -1,8 +1,9 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + export const GET = async (req: MedusaRequest, res: MedusaResponse) => { - const id = req.auth_user!.app_metadata.customer_id + const id = req.auth_user!.app_metadata?.customer_id const customerModule = req.scope.resolve(ModuleRegistrationName.CUSTOMER) diff --git a/packages/medusa/src/api-v2/store/customers/middlewares.ts b/packages/medusa/src/api-v2/store/customers/middlewares.ts index c60b5f1ea2598..f0bf0e8c2821a 100644 --- a/packages/medusa/src/api-v2/store/customers/middlewares.ts +++ b/packages/medusa/src/api-v2/store/customers/middlewares.ts @@ -7,9 +7,10 @@ import { StorePostCustomersMeAddressesAddressReq, StoreGetCustomersMeAddressesParams, } from "./validators" -import authenticate from "../../../utils/authenticate-middleware" import * as QueryConfig from "./query-config" +import { authenticate } from "../../../utils/authenticate-middleware" + export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [ { method: "ALL", diff --git a/packages/medusa/src/api-v2/store/customers/query-config.ts b/packages/medusa/src/api-v2/store/customers/query-config.ts index 5a54fd3bcea0e..7a503843c647f 100644 --- a/packages/medusa/src/api-v2/store/customers/query-config.ts +++ b/packages/medusa/src/api-v2/store/customers/query-config.ts @@ -17,7 +17,7 @@ export const defaultStoreCustomersFields: (keyof CustomerDTO)[] = [ ] export const retrieveTransformQueryConfig = { - defaultFields: defaultStoreCustomersFields, + defaultFields: defaultStoreCustomersFields as string[], defaultRelations: defaultStoreCustomersRelations, allowedRelations: allowedStoreCustomersRelations, isList: false, diff --git a/packages/medusa/src/api-v2/store/customers/route.ts b/packages/medusa/src/api-v2/store/customers/route.ts index b7600fc55dd78..14fb4f1653e91 100644 --- a/packages/medusa/src/api-v2/store/customers/route.ts +++ b/packages/medusa/src/api-v2/store/customers/route.ts @@ -1,8 +1,30 @@ import { MedusaRequest, MedusaResponse } from "../../../types/routing" -import { createCustomerAccountWorkflow } from "@medusajs/core-flows" + +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { CreateCustomerDTO } from "@medusajs/types" +import { createCustomerAccountWorkflow } from "@medusajs/core-flows" export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + if (req.auth_user?.app_metadata?.customer_id) { + const remoteQuery = req.scope.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const query = remoteQueryObjectFromString({ + entryPoint: "customer", + variables: { id: req.auth_user.app_metadata.customer_id }, + fields: [], + }) + const [customer] = await remoteQuery(query) + + res.status(200).json({ customer }) + + return + } + const createCustomers = createCustomerAccountWorkflow(req.scope) const customersData = req.validatedBody as CreateCustomerDTO @@ -10,5 +32,9 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { input: { customersData, authUserId: req.auth_user!.id }, }) + // Set customer_id on session user if we are in session + if (req.session.auth_user) { + req.session.auth_user.app_metadata.customer_id = result.id + } res.status(200).json({ customer: result }) } diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index 8a17b7eee061c..d270d7adc87cc 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -1,11 +1,13 @@ +import type { Customer, User } from "../models" import type { NextFunction, Request, Response } from "express" -import type { Customer, User } from "../models" +import { AuthUserDTO } from "@medusajs/types" import type { MedusaContainer } from "./global" export interface MedusaRequest extends Request { user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer + session?: any requestId?: string auth_user?: { id: string; app_metadata: Record; scope: string } } diff --git a/packages/medusa/src/utils/authenticate-middleware.ts b/packages/medusa/src/utils/authenticate-middleware.ts index 0417017ef4ce3..ffc2326a09f53 100644 --- a/packages/medusa/src/utils/authenticate-middleware.ts +++ b/packages/medusa/src/utils/authenticate-middleware.ts @@ -1,22 +1,20 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { AuthUserDTO, IAuthModuleService } from "@medusajs/types" -import { NextFunction, RequestHandler } from "express" import { MedusaRequest, MedusaResponse } from "../types/routing" +import { NextFunction, RequestHandler } from "express" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" const SESSION_AUTH = "session" const BEARER_AUTH = "bearer" type MedusaSession = { - auth: { - [authScope: string]: { - user_id: string - } - } + auth_user: AuthUserDTO + scope: string } type AuthType = "session" | "bearer" -export default ( +export const authenticate = ( authScope: string, authType: AuthType | AuthType[], options: { allowUnauthenticated?: boolean } = {} @@ -36,19 +34,18 @@ export default ( let authUser: AuthUserDTO | null = null if (authTypes.includes(SESSION_AUTH)) { - if (session.auth && session.auth[authScope]) { - authUser = await authModule - .retrieveAuthUser(session.auth[authScope].user_id) - .catch(() => null) + if (session.auth_user && session.scope === authScope) { + authUser = session.auth_user } } - if (authTypes.includes(BEARER_AUTH)) { + if (!authUser && authTypes.includes(BEARER_AUTH)) { const authHeader = req.headers.authorization if (authHeader) { const re = /(\S+)\s+(\S+)/ const matches = authHeader.match(re) + // TODO: figure out how to obtain token (and store correct data in token) if (matches) { const tokenType = matches[1] const token = matches[2] diff --git a/packages/types/src/auth/common/auth-provider.ts b/packages/types/src/auth/common/auth-provider.ts index e3653642136f8..3f4122d42f4cd 100644 --- a/packages/types/src/auth/common/auth-provider.ts +++ b/packages/types/src/auth/common/auth-provider.ts @@ -3,7 +3,7 @@ import { BaseFilterable } from "../../dal" export type AuthProviderDTO = { provider: string name: string - domain: ProviderDomain + scope?: string is_active: boolean config: Record | null } @@ -11,29 +11,22 @@ export type AuthProviderDTO = { export type CreateAuthProviderDTO = { provider: string name: string - domain?: ProviderDomain + scope?: string is_active?: boolean - config?: Record + config?: Record } export type UpdateAuthProviderDTO = { provider: string name?: string - domain?: ProviderDomain is_active?: boolean config?: Record } -export enum ProviderDomain { - ALL = "all", - STORE = "store", - ADMIN = "admin", -} - export interface FilterableAuthProviderProps extends BaseFilterable { provider?: string[] is_active?: boolean - domain?: ProviderDomain[] + scope?: string[] name?: string[] } diff --git a/packages/types/src/auth/common/auth-user.ts b/packages/types/src/auth/common/auth-user.ts index 11357a10ebe8a..1946e360830a2 100644 --- a/packages/types/src/auth/common/auth-user.ts +++ b/packages/types/src/auth/common/auth-user.ts @@ -1,10 +1,11 @@ -import { BaseFilterable } from "../../dal" import { AuthProviderDTO } from "./auth-provider" +import { BaseFilterable } from "../../dal" export type AuthUserDTO = { id: string provider_id: string entity_id: string + scope: string provider: AuthProviderDTO provider_metadata?: Record user_metadata: Record @@ -12,8 +13,9 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { - provider_id: string + provider: string entity_id: string + scope: string provider_metadata?: Record user_metadata?: Record app_metadata?: Record diff --git a/packages/types/src/auth/common/provider.ts b/packages/types/src/auth/common/provider.ts index 03dfb74b2e0f5..a13f90052d41d 100644 --- a/packages/types/src/auth/common/provider.ts +++ b/packages/types/src/auth/common/provider.ts @@ -10,13 +10,13 @@ export type AuthModuleProviderConfig = { scopes: Record } -export type AuthProviderScope = { domain?: string } & Record +export type AuthProviderScope = Record export type AuthenticationInput = { - connection: { encrypted: boolean } url: string headers: Record query: Record body: Record - scope: string + authScope: string + protocol: string } diff --git a/yarn.lock b/yarn.lock index 5775225d010e6..bed468a8136a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7902,7 +7902,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.3.1 jest: ^29.6.3 jsonwebtoken: ^9.0.2 knex: 2.4.2 @@ -8022,7 +8022,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.3.1 jest: ^29.6.3 knex: 2.4.2 medusa-test-utils: ^1.1.40 From abc30517cb22dece094bd74155e673c11ea89626 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 2 Feb 2024 12:39:34 +0100 Subject: [PATCH 2/4] feat(): added percentage calculations for all cases (#6300) what: - adds missing percentage calculations for items and shipping methods --- .../promotion-module/compute-actions.spec.ts | 5141 +++++++++++------ .../promotion-module/promotion.spec.ts | 29 +- .../src/utils/compute-actions/buy-get.ts | 7 +- .../src/utils/compute-actions/items.ts | 38 +- .../utils/compute-actions/shipping-methods.ts | 21 +- .../utils/validations/application-method.ts | 12 + .../src/promotion/common/compute-actions.ts | 4 +- 7 files changed, 3614 insertions(+), 1638 deletions(-) diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index ee44ad54453b3..7ba8ffcd66f21 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -1,6 +1,6 @@ import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" -import { PromotionType } from "@medusajs/utils" +import { ApplicationMethodType, PromotionType } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" @@ -49,7 +49,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -60,7 +60,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -95,7 +95,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, adjustments: [ { id: "test-adjustment", @@ -106,7 +106,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, }, ], }) @@ -138,12 +138,12 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, }, { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, adjustments: [ { id: "test-adjustment", @@ -162,221 +162,1789 @@ describe("Promotion Service: computeActions", () => { }) describe("when promotion is for items and allocation is each", () => { - it("should compute the correct item amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ + describe("when application type is fixed", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "200", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 100, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 5, + subtotal: 750, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "200", - max_quantity: 1, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "30", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - items: [ + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", + customer: { + customer_group: { + id: "VIP", + }, }, - product: { - id: "prod_tshirt", + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 50, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, }, + ]) + + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, - product: { - id: "prod_sweater", + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", }, - ], + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute the correct item amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - is_automatic: true, - type: PromotionType.STANDARD, - rules: [ + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "200", - max_quantity: 1, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 500, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 100, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 5, + subtotal: 750, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 10, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "30", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 3, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 45, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 2, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 10.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 10, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 10, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 10000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + }) + + describe("when promotion is for items and allocation is across", () => { + describe("when application type is fixed", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "400", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 300, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "400", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 300, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "30", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "50", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 7.5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 22.5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 12.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 37.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1000", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "50", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1500", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "500", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 60, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 60, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 4.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 13.5, + code: "PROMOTION_TEST_2", }, - }, - items: [ + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, }, + ]) + + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + ]) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "30", - max_quantity: 2, - target_rules: [ + items: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, }, ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 5, + code: "PROMOTION_TEST", }, - }, - ]) + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 4.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "50", - max_quantity: 1, - target_rules: [ + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -385,8 +1953,8 @@ describe("Promotion Service: computeActions", () => { items: [ { id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, + quantity: 5, + subtotal: 5000, product_category: { id: "catg_cotton", }, @@ -394,976 +1962,1128 @@ describe("Promotion Service: computeActions", () => { id: "prod_tshirt", }, }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, product_category: { id: "catg_cotton", }, product: { - id: "prod_sweater", + id: "prod_tshirt", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 30, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 30, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 20, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 50, - code: "PROMOTION_TEST_2", - }, - ]) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 2, - target_rules: [ + describe("when promotion is for shipping_method and allocation is each", () => { + describe("when application type is fixed", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "50", - max_quantity: 1, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", }, }, - items: [ + shipping_methods: [ { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", }, }, { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", }, }, - ], - } - ) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 50, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 5, - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) + }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", }, - }, - items: [ { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", }, - ], + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 5, - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) - - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - }) + ]) - describe("when promotion is for items and allocation is across", () => { - it("should compute the correct item amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + [], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "400", - target_rules: [ + shipping_methods: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], }, - }, - ]) + { prevent_auto_promotions: true } + ) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 2, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - { - id: "item_cotton_sweater", - quantity: 2, - unit_price: 300, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, - }, - ], + expect(result).toEqual([]) }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 300, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "400", - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 2, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "item_cotton_sweater", - quantity: 2, - unit_price: 300, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 300, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "500", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "30", - target_rules: [ + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "50", - target_rules: [ + shipping_methods: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 250, + code: "PROMOTION_TEST", }, - }, - ]) + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "1200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", }, }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, + shipping_methods: [ { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 7.5, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 22.5, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, - ]) - }) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "500", - target_rules: [ + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "1200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "50", - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", }, }, - items: [ + shipping_methods: [ { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", }, }, { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, - product_category: { - id: "catg_cotton", + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", }, - product: { - id: "prod_sweater", + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", }, }, ], - } - ) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 50, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, - ]) - }) + }) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "1500", - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, }, - product: { - id: "prod_tshirt", + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, + ], + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) + it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + [], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "500", - target_rules: [ + shipping_methods: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], }, - }, - ]) + { prevent_auto_promotions: true } + ) - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, + expect(result).toEqual([]) }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - }) + ]) - describe("when promotion is for shipping_method and allocation is each", () => { - it("should compute the correct shipping_method amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_express", - unit_price: 250, - shipping_option: { - id: "express", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", }, { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 22.5, + code: "PROMOTION_TEST_2", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotionTwo] = await service.create([ { - id: "shipping_method_express", - unit_price: 250, - shipping_option: { - id: "express", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 22.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "100", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - [], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1372,93 +3092,111 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 1200, shipping_option: { id: "express", }, }, - { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, ], - }, - { prevent_auto_promotions: true } - ) + }) - expect(result).toEqual([]) - }) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + }) + + describe("when promotion is for shipping_method and allocation is across", () => { + describe("when application type is fixed", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1467,111 +3205,74 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 50, - code: "PROMOTION_TEST_2", - }, - ]) - }) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "500", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", }, - }, - ]) + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", + }, + ]) + }) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -1580,369 +3281,356 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 250, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "1200", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) + }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", }, - }, - shipping_methods: [ { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", }, - ], + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "1200", - max_quantity: 2, - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) - - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotion2] = await service.create([ { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - }) + ]) - describe("when promotion is for shipping_method and allocation is across", () => { - it("should compute the correct shipping_method amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + shipping_methods: [ { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - }, - }, - ]) + } + ) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + expect(result).toEqual([ { - id: "shipping_method_express", - unit_price: 500, - shipping_option: { - id: "express", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", }, - { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", - }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 83.33333333333331, + code: "PROMOTION_TEST_2", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 16.66666666666667, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1000", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotion2] = await service.create([ { - id: "shipping_method_express", - unit_price: 500, - shipping_option: { - id: "express", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 500, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 100, + code: "PROMOTION_TEST", }, - ], + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotion2] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1951,115 +3639,126 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 1200, shipping_option: { id: "express", }, }, - { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 83.33333333333331, - code: "PROMOTION_TEST_2", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 16.66666666666667, - code: "PROMOTION_TEST_2", - }, - ]) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1000", - target_rules: [ + describe("when application type is percentage", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotion2] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -2068,154 +3767,376 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 500, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 100, - code: "PROMOTION_TEST", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1200", - target_rules: [ + shipping_methods: [ { - attribute: "shipping_option.id", + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 45, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 9, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - shipping_methods: [ + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 500, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 100, + code: "PROMOTION_TEST", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1200", - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", }, }, - ], - }) + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) }) @@ -2251,7 +4172,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2262,7 +4183,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 150, + subtotal: 300, product_category: { id: "catg_cotton", }, @@ -2321,7 +4242,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2332,7 +4253,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 150, + subtotal: 300, product_category: { id: "catg_cotton", }, @@ -2412,7 +4333,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 50, product_category: { id: "catg_cotton", }, @@ -2423,7 +4344,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 1, - unit_price: 150, + subtotal: 150, product_category: { id: "catg_cotton", }, @@ -2516,7 +4437,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 50, product_category: { id: "catg_cotton", }, @@ -2527,7 +4448,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 1, - unit_price: 150, + subtotal: 150, product_category: { id: "catg_cotton", }, @@ -2552,18 +4473,6 @@ describe("Promotion Service: computeActions", () => { amount: 150, code: "PROMOTION_TEST", }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, ]) }) }) @@ -2615,7 +4524,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2632,7 +4541,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -2708,7 +4617,7 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, @@ -2721,14 +4630,14 @@ describe("Promotion Service: computeActions", () => { }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, @@ -2770,7 +4679,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2781,7 +4690,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2792,7 +4701,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -2862,7 +4771,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2873,7 +4782,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2884,7 +4793,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -2947,7 +4856,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2958,7 +4867,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2969,7 +4878,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -3045,7 +4954,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -3056,7 +4965,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -3067,7 +4976,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 909f31dbc49ac..dc36e066132d7 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -1,16 +1,17 @@ +import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" import { + ApplicationMethodTargetType, ApplicationMethodType, CampaignBudgetType, PromotionType, } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" import { createPromotions } from "../../../__fixtures__/promotion" import { MikroOrmWrapper } from "../../../utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" -import { Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" jest.setTimeout(30000) @@ -114,6 +115,24 @@ describe("Promotion Service", () => { ) }) + it("should throw error when percentage type and value is greater than 100", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: ApplicationMethodTargetType.ORDER, + value: "1000", + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "Application Method value should be a percentage number between 0 and 100" + ) + }) + it("should throw an error when both campaign and campaign_id are provided", async () => { const startsAt = new Date("01/01/2023") const endsAt = new Date("01/01/2023") @@ -655,7 +674,7 @@ describe("Promotion Service", () => { is_automatic: true, code: "TEST", type: PromotionType.BUYGET, - }, + } as any, ]) expect(updatedPromotion).toEqual( @@ -899,12 +918,12 @@ describe("Promotion Service", () => { value: "200", target_type: "items", }, - }, + } as any, { id: "promotion-id-2", code: "PROMOTION_2", type: PromotionType.STANDARD, - }, + } as any, ]) }) diff --git a/packages/promotion/src/utils/compute-actions/buy-get.ts b/packages/promotion/src/utils/compute-actions/buy-get.ts index cc6eaa44e114f..d3bcc3f68d249 100644 --- a/packages/promotion/src/utils/compute-actions/buy-get.ts +++ b/packages/promotion/src/utils/compute-actions/buy-get.ts @@ -46,7 +46,10 @@ export function getComputedActionsForBuyGet( const validItemsForTargetRules = itemsContext .filter((item) => areRulesValidForContext(targetRules, item)) .sort((a, b) => { - return b.unit_price - a.unit_price + const aPrice = a.subtotal / a.quantity + const bPrice = b.subtotal / b.quantity + + return bPrice - aPrice }) let remainingQtyToApply = applyToQuantity @@ -54,7 +57,7 @@ export function getComputedActionsForBuyGet( for (const method of validItemsForTargetRules) { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 const multiplier = Math.min(method.quantity, remainingQtyToApply) - const amount = method.unit_price * multiplier + const amount = (method.subtotal / method.quantity) * multiplier const newRemainingQtyToApply = remainingQtyToApply - multiplier if (newRemainingQtyToApply < 0 || amount <= 0) { diff --git a/packages/promotion/src/utils/compute-actions/items.ts b/packages/promotion/src/utils/compute-actions/items.ts index afbf400b564ee..555a27548f612 100644 --- a/packages/promotion/src/utils/compute-actions/items.ts +++ b/packages/promotion/src/utils/compute-actions/items.ts @@ -5,6 +5,7 @@ import { import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, ComputedActions, MedusaError, } from "@medusajs/utils" @@ -67,10 +68,15 @@ export function applyPromotionToItems( method.quantity, applicationMethod?.max_quantity! ) - const promotionValue = - parseFloat(applicationMethod!.value!) * quantityMultiplier - const applicableTotal = - method.unit_price * quantityMultiplier - appliedPromoValue + const totalItemValue = + (method.subtotal / method.quantity) * quantityMultiplier + let promotionValue = parseFloat(applicationMethod!.value!) + const applicableTotal = totalItemValue - appliedPromoValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + promotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(promotionValue, applicableTotal) if (amount <= 0) { @@ -106,18 +112,32 @@ export function applyPromotionToItems( ) { const totalApplicableValue = items!.reduce((acc, method) => { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return acc + method.unit_price * method.quantity - appliedPromoValue + return ( + acc + + (method.subtotal / method.quantity) * method.quantity - + appliedPromoValue + ) }, 0) for (const method of items!) { - const promotionValue = parseFloat(applicationMethod!.value!) const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const promotionValue = parseFloat(applicationMethod!.value!) const applicableTotal = - method.unit_price * method.quantity - appliedPromoValue + (method.subtotal / method.quantity) * method.quantity - + appliedPromoValue + + if (applicableTotal <= 0) { + continue + } // TODO: should we worry about precision here? - const applicablePromotionValue = + let applicablePromotionValue = (applicableTotal / totalApplicableValue) * promotionValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + applicablePromotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(applicablePromotionValue, applicableTotal) if (amount <= 0) { @@ -135,6 +155,8 @@ export function applyPromotionToItems( continue } + methodIdPromoValueMap.set(method.id, appliedPromoValue + amount) + computedActions.push({ action: ComputedActions.ADD_ITEM_ADJUSTMENT, item_id: method.id, diff --git a/packages/promotion/src/utils/compute-actions/shipping-methods.ts b/packages/promotion/src/utils/compute-actions/shipping-methods.ts index 440021f482bdf..6e96db1fd2aec 100644 --- a/packages/promotion/src/utils/compute-actions/shipping-methods.ts +++ b/packages/promotion/src/utils/compute-actions/shipping-methods.ts @@ -2,6 +2,7 @@ import { PromotionTypes } from "@medusajs/types" import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, ComputedActions, MedusaError, } from "@medusajs/utils" @@ -55,8 +56,13 @@ export function applyPromotionToShippingMethods( if (allocation === ApplicationMethodAllocation.EACH) { for (const method of shippingMethods!) { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - const promotionValue = parseFloat(applicationMethod!.value!) - const applicableTotal = method.unit_price - appliedPromoValue + let promotionValue = parseFloat(applicationMethod!.value!) + const applicableTotal = method.subtotal - appliedPromoValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + promotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(promotionValue, applicableTotal) if (amount <= 0) { @@ -89,7 +95,7 @@ export function applyPromotionToShippingMethods( const totalApplicableValue = shippingMethods!.reduce((acc, method) => { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return acc + method.unit_price - appliedPromoValue + return acc + method.subtotal - appliedPromoValue }, 0) if (totalApplicableValue <= 0) { @@ -98,14 +104,19 @@ export function applyPromotionToShippingMethods( for (const method of shippingMethods!) { const promotionValue = parseFloat(applicationMethod!.value!) - const applicableTotal = method.unit_price + const applicableTotal = method.subtotal const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 // TODO: should we worry about precision here? - const applicablePromotionValue = + let applicablePromotionValue = (applicableTotal / totalApplicableValue) * promotionValue - appliedPromoValue + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + applicablePromotionValue = + (promotionValue / 100) * (applicableTotal - appliedPromoValue) + } + const amount = Math.min(applicablePromotionValue, applicableTotal) if (amount <= 0) { diff --git a/packages/promotion/src/utils/validations/application-method.ts b/packages/promotion/src/utils/validations/application-method.ts index 99c6296349cc4..7d022f0e9d519 100644 --- a/packages/promotion/src/utils/validations/application-method.ts +++ b/packages/promotion/src/utils/validations/application-method.ts @@ -37,11 +37,23 @@ export function validateApplicationMethodAttributes( const applyToQuantity = data.apply_to_quantity || applicationMethod?.apply_to_quantity const targetType = data.target_type || applicationMethod?.target_type + const type = data.type || applicationMethod?.type const applicationMethodType = data.type || applicationMethod?.type + const value = parseFloat(data.value! || applicationMethod?.value!) const maxQuantity = data.max_quantity || applicationMethod.max_quantity const allocation = data.allocation || applicationMethod.allocation const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType) + if ( + type === ApplicationMethodType.PERCENTAGE && + (value <= 0 || value > 100) + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Application Method value should be a percentage number between 0 and 100` + ) + } + if (promotion?.type === PromotionType.BUYGET) { if (!isPresent(applyToQuantity)) { throw new MedusaError( diff --git a/packages/types/src/promotion/common/compute-actions.ts b/packages/types/src/promotion/common/compute-actions.ts index 267da4b5c38d9..1384daa93fe5f 100644 --- a/packages/types/src/promotion/common/compute-actions.ts +++ b/packages/types/src/promotion/common/compute-actions.ts @@ -51,13 +51,13 @@ export interface ComputeActionAdjustmentLine extends Record { export interface ComputeActionItemLine extends Record { id: string quantity: number - unit_price: number + subtotal: number adjustments?: ComputeActionAdjustmentLine[] } export interface ComputeActionShippingLine extends Record { id: string - unit_price: number + subtotal: number adjustments?: ComputeActionAdjustmentLine[] } From a7be5d7b6d4a3ee232e4657e141bc23de31ece39 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Fri, 2 Feb 2024 15:20:32 +0100 Subject: [PATCH 3/4] chore: Abstract module service (#6188) **What** - Remove services that do not have any custom business and replace them with a simple interfaces - Abstract module service provide the following base implementation - retrieve - list - listAndCount - delete - softDelete - restore The above methods are created for the main model and also for each other models for which a config is provided all method such as list, listAndCount, delete, softDelete and restore are pluralized with the model it refers to **Migration** - [x] product - [x] pricing - [x] promotion - [x] cart - [x] auth - [x] customer - [x] payment - [x] Sales channel - [x] Workflow-* **Usage** **Module** The module service can now extend the ` ModulesSdkUtils.abstractModuleServiceFactory` which returns a class with the default implementation for each method and each model following the standard naming convention mentioned above. This factory have 3 template arguments being the container, the main model DTO and an object representing the other model with a config object that contains at list the DTO and optionally a singular and plural property in case it needs to be set manually. It looks like the following: ```ts export default class PricingModuleService extends ModulesSdkUtils.abstractModuleServiceFactory< InjectedDependencies, PricingTypes.PriceSetDTO, { Currency: { dto: PricingTypes.CurrencyDTO } MoneyAmount: { dto: PricingTypes.MoneyAmountDTO } PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO } PriceSetMoneyAmountRules: { dto: PricingTypes.PriceSetMoneyAmountRulesDTO } PriceRule: { dto: PricingTypes.PriceRuleDTO } RuleType: { dto: PricingTypes.RuleTypeDTO } PriceList: { dto: PricingTypes.PriceListDTO } PriceListRule: { dto: PricingTypes.PriceListRuleDTO } } >(PriceSet, generateMethodForModels, entityNameToLinkableKeysMap) implements PricingTypes.IPricingModuleService { // ... } ``` In the above, the singular and plural can be inferred as there is no tricky naming. Also, the default implementation does not remove the fact that you need to provides all the overloads etc in your module service interface. The above will provide a default implementation following the interface `AbstractModuleService` which is also auto generated, hence you will have the following methods available: **for the main model** - list - retrieve - listAndCount - delete - softDelete - restore **for the other models** - list**MyModels** - retrieve**MyModel** - listAndCount**MyModels** - delete**MyModels** - softDelete**MyModels** - restore**MyModels** **Internal module service** The internal module service can now extend `ModulesSdkUtils.internalModuleServiceFactory` which takes only one template argument which is the container type. All internal services provides a default implementation for all retrieve, list, listAndCount, create, update, delete, softDelete, restore methods which follow the following interface `ModulesSdkTypes.InternalModuleService`: ```ts export interface InternalModuleService< TEntity extends {}, TContainer extends object = object > { get __container__(): TContainer retrieve( idOrObject: string, config?: FindConfig, sharedContext?: Context ): Promise retrieve( idOrObject: object, config?: FindConfig, sharedContext?: Context ): Promise list( filters?: FilterQuery | BaseFilterable>, config?: FindConfig, sharedContext?: Context ): Promise listAndCount( filters?: FilterQuery | BaseFilterable>, config?: FindConfig, sharedContext?: Context ): Promise<[TEntity[], number]> create(data: any[], sharedContext?: Context): Promise create(data: any, sharedContext?: Context): Promise update(data: any[], sharedContext?: Context): Promise update(data: any, sharedContext?: Context): Promise update( selectorAndData: { selector: FilterQuery | BaseFilterable> data: any }, sharedContext?: Context ): Promise update( selectorAndData: { selector: FilterQuery | BaseFilterable> data: any }[], sharedContext?: Context ): Promise delete(idOrSelector: string, sharedContext?: Context): Promise delete(idOrSelector: string[], sharedContext?: Context): Promise delete(idOrSelector: object, sharedContext?: Context): Promise delete(idOrSelector: object[], sharedContext?: Context): Promise delete( idOrSelector: { selector: FilterQuery | BaseFilterable> }, sharedContext?: Context ): Promise softDelete( idsOrFilter: string[] | InternalFilterQuery, sharedContext?: Context ): Promise<[TEntity[], Record]> restore( idsOrFilter: string[] | InternalFilterQuery, sharedContext?: Context ): Promise<[TEntity[], Record]> upsert(data: any[], sharedContext?: Context): Promise upsert(data: any, sharedContext?: Context): Promise } ``` When a service is auto generated you can use that interface to type your class property representing the expected internal service. **Repositories** The repositories can now extend `DALUtils.mikroOrmBaseRepositoryFactory` which takes one template argument being the entity or the template entity and provides all the default implementation. If the repository is auto generated you can type it using the `RepositoryService` interface. Here is the new interface typings. ```ts export interface RepositoryService extends BaseRepositoryService { find(options?: FindOptions, context?: Context): Promise findAndCount( options?: FindOptions, context?: Context ): Promise<[T[], number]> create(data: any[], context?: Context): Promise // Becareful here, if you have a custom internal service, the update data should never be the entity otherwise // both entity and update will point to the same ref and create issues with mikro orm update(data: { entity; update }[], context?: Context): Promise delete( idsOrPKs: FilterQuery & BaseFilterable>, context?: Context ): Promise /** * Soft delete entities and cascade to related entities if configured. * * @param idsOrFilter * @param context * * @returns [T[], Record] the second value being the map of the entity names and ids that were soft deleted */ softDelete( idsOrFilter: string[] | InternalFilterQuery, context?: Context ): Promise<[T[], Record]> restore( idsOrFilter: string[] | InternalFilterQuery, context?: Context ): Promise<[T[], Record]> upsert(data: any[], context?: Context): Promise } ``` --- .../customer/store/list-customer-addresses.ts | 20 +- .../admin/create-product-variant.spec.ts | 5 +- .../workflows/product/create-product.ts | 4 +- .../services/auth-provider/index.spec.ts | 6 +- .../services/auth-user/index.spec.ts | 4 +- .../services/module/auth-provider.spec.ts | 22 +- .../services/module/auth-user.spec.ts | 30 +- .../services/module/providers.spec.ts | 3 +- packages/auth/src/providers/google.ts | 17 +- packages/auth/src/services/auth-module.ts | 221 ++--- packages/auth/src/services/auth-provider.ts | 24 - packages/auth/src/services/auth-user.ts | 25 +- packages/auth/src/services/index.ts | 1 - packages/auth/src/types/repositories/index.ts | 26 - packages/cart/src/services/address.ts | 23 - packages/cart/src/services/cart-module.ts | 346 ++------ packages/cart/src/services/cart.ts | 23 - packages/cart/src/services/index.ts | 9 - .../cart/src/services/line-item-adjustment.ts | 26 - .../cart/src/services/line-item-tax-line.ts | 26 - packages/cart/src/services/line-item.ts | 23 - .../services/shipping-method-adjustment.ts | 26 - .../src/services/shipping-method-tax-line.ts | 22 - packages/cart/src/services/shipping-method.ts | 23 - packages/cart/src/types/index.ts | 1 - packages/cart/src/types/repositories.ts | 114 --- packages/cart/src/types/shipping-method.ts | 2 +- .../steps/delete-customer-groups.ts | 4 +- .../steps/update-customer-groups.ts | 6 +- .../src/customer/steps/create-addresses.ts | 6 +- .../src/customer/steps/delete-addresses.ts | 2 +- .../maybe-unset-default-billing-addresses.ts | 8 +- .../maybe-unset-default-shipping-addresses.ts | 8 +- .../src/customer/steps/update-addresses.ts | 4 +- .../steps/utils/unset-address-for-create.ts | 2 +- .../steps/utils/unset-address-for-update.ts | 2 +- .../services/customer-module/index.spec.ts | 32 +- packages/customer/src/models/address.ts | 15 +- packages/customer/src/services/address.ts | 23 - .../src/services/customer-group-customer.ts | 25 - .../customer/src/services/customer-group.ts | 23 - .../customer/src/services/customer-module.ts | 618 ++++---------- packages/customer/src/services/customer.ts | 22 - packages/customer/src/services/index.ts | 4 - packages/customer/src/types/index.ts | 5 +- .../src/types/{ => services}/address.ts | 0 .../types/services/customer-group-customer.ts | 5 + packages/customer/src/types/services/index.ts | 2 + .../services/payment-module/index.spec.ts | 2 +- packages/payment/src/services/index.ts | 1 - .../src/services/payment-collection.ts | 26 - .../payment/src/services/payment-module.ts | 132 +-- packages/payment/src/types/repositories.ts | 18 - .../__tests__/services/currency/index.spec.ts | 2 +- .../services/money-amount/index.spec.ts | 2 +- .../services/price-list-rule/index.spec.ts | 2 +- .../services/price-list/index.spec.ts | 2 +- .../services/price-rule/index.spec.ts | 2 +- .../index.spec.ts | 2 +- .../services/price-set/index.spec.ts | 2 +- .../services/pricing-module/currency.spec.ts | 2 +- .../pricing-module/money-amount.spec.ts | 6 +- .../pricing-module/price-list-rule.spec.ts | 8 +- .../pricing-module/price-list.spec.ts | 2 +- .../pricing-module/price-rule.spec.ts | 2 +- .../price-set-money-amount-rules.spec.ts | 2 +- .../services/pricing-module/price-set.spec.ts | 2 +- .../services/pricing-module/rule-type.spec.ts | 2 +- .../services/rule-type/index.spec.ts | 2 +- .../src/services/__fixtures__/currency.ts | 11 +- .../src/services/__tests__/currency.spec.ts | 43 +- packages/pricing/src/services/currency.ts | 23 - packages/pricing/src/services/index.ts | 6 - packages/pricing/src/services/money-amount.ts | 27 - .../src/services/price-list-rule-value.ts | 32 +- .../pricing/src/services/price-list-rule.ts | 55 +- packages/pricing/src/services/price-list.ts | 41 +- packages/pricing/src/services/price-rule.ts | 29 +- .../services/price-set-money-amount-rules.ts | 27 - .../src/services/price-set-money-amount.ts | 27 - .../src/services/price-set-rule-type.ts | 27 - packages/pricing/src/services/price-set.ts | 28 - .../pricing/src/services/pricing-module.ts | 785 ++---------------- packages/pricing/src/services/rule-type.ts | 54 +- .../pricing/src/types/repositories/index.ts | 157 ---- .../services/product-collection/index.ts | 2 +- .../product-module-service/products.spec.ts | 16 + .../services/product-option/index.ts | 2 +- .../__tests__/services/product-tag/index.ts | 2 +- .../__tests__/services/product-type/index.ts | 2 +- .../services/product-variant/index.ts | 2 +- .../__tests__/services/product/index.ts | 4 +- packages/product/src/models/product.ts | 2 +- .../product/src/repositories/product-image.ts | 2 +- packages/product/src/repositories/product.ts | 23 +- .../src/services/__fixtures__/product.ts | 10 +- .../src/services/__tests__/product.spec.ts | 43 +- packages/product/src/services/index.ts | 2 - .../src/services/product-collection.ts | 74 +- .../product/src/services/product-image.ts | 18 - .../src/services/product-module-service.ts | 657 ++------------- .../src/services/product-option-value.ts | 23 - .../product/src/services/product-option.ts | 25 +- packages/product/src/services/product-tag.ts | 25 +- packages/product/src/services/product-type.ts | 25 +- .../product/src/services/product-variant.ts | 18 +- packages/product/src/services/product.ts | 29 +- packages/product/src/types/index.ts | 1 - packages/product/src/types/repositories.ts | 100 --- .../promotion-module/campaign.spec.ts | 6 +- .../promotion-module/promotion.spec.ts | 16 +- .../promotion/src/repositories/campaign.ts | 14 +- .../src/services/application-method.ts | 27 - .../promotion/src/services/campaign-budget.ts | 27 - packages/promotion/src/services/campaign.ts | 27 - packages/promotion/src/services/index.ts | 6 - .../src/services/promotion-module.ts | 358 ++------ .../src/services/promotion-rule-value.ts | 30 - .../promotion/src/services/promotion-rule.ts | 27 - packages/promotion/src/services/promotion.ts | 27 - packages/promotion/src/types/index.ts | 1 - packages/promotion/src/types/repositories.ts | 91 -- .../services/__fixtures__/sales-channel.ts | 12 +- .../services/__tests__/sales-channle.spec.ts | 22 +- packages/sales-channel/src/services/index.ts | 1 - .../src/services/sales-channel-module.ts | 174 +--- .../src/services/sales-channel.ts | 24 - .../sales-channel/src/types/repositories.ts | 15 - .../types/src/auth/common/auth-provider.ts | 1 + packages/types/src/auth/service.ts | 4 +- packages/types/src/common/common.ts | 22 +- packages/types/src/customer/service.ts | 46 +- packages/types/src/dal/repository-service.ts | 34 +- packages/types/src/modules-sdk/index.ts | 1 + .../modules-sdk/internal-module-service.ts | 81 ++ .../types/src/modules-sdk/module-service.ts | 0 packages/types/src/payment/service.ts | 4 +- packages/types/src/pricing/service.ts | 19 +- packages/types/src/product/service.ts | 30 +- .../src/common/__tests__/pluralize.spec.ts | 33 + packages/utils/src/common/index.ts | 1 + packages/utils/src/common/plurailze.ts | 27 + packages/utils/src/dal/index.ts | 1 - .../src/dal/mikro-orm/mikro-orm-repository.ts | 210 ++--- packages/utils/src/dal/mikro-orm/utils.ts | 33 +- packages/utils/src/dal/repository.ts | 104 --- .../abstract-module-service-factory.spec.ts | 201 +++++ .../internal-module-service-factory.spec.ts | 240 ++++++ .../abstract-module-service-factory.ts | 527 ++++++++++++ .../modules-sdk/abstract-service-factory.ts | 257 ------ .../modules-sdk/decorators/inject-manager.ts | 11 +- .../decorators/inject-transaction-manager.ts | 10 +- packages/utils/src/modules-sdk/index.ts | 3 +- .../internal-module-service-factory.ts | 442 ++++++++++ .../loaders/container-loader-factory.ts | 4 +- .../src/services/index.ts | 1 - .../src/services/workflow-execution.ts | 21 - .../src/services/workflows-module.ts | 13 +- .../utils/workflow-orchestrator-storage.ts | 10 +- .../src/services/index.ts | 1 - .../src/services/workflow-execution.ts | 21 - .../src/services/workflows-module.ts | 15 +- .../utils/workflow-orchestrator-storage.ts | 6 +- 163 files changed, 2857 insertions(+), 5070 deletions(-) delete mode 100644 packages/auth/src/services/auth-provider.ts delete mode 100644 packages/cart/src/services/address.ts delete mode 100644 packages/cart/src/services/cart.ts delete mode 100644 packages/cart/src/services/line-item-adjustment.ts delete mode 100644 packages/cart/src/services/line-item-tax-line.ts delete mode 100644 packages/cart/src/services/line-item.ts delete mode 100644 packages/cart/src/services/shipping-method-adjustment.ts delete mode 100644 packages/cart/src/services/shipping-method-tax-line.ts delete mode 100644 packages/cart/src/services/shipping-method.ts delete mode 100644 packages/cart/src/types/repositories.ts delete mode 100644 packages/customer/src/services/address.ts delete mode 100644 packages/customer/src/services/customer-group-customer.ts delete mode 100644 packages/customer/src/services/customer-group.ts delete mode 100644 packages/customer/src/services/customer.ts rename packages/customer/src/types/{ => services}/address.ts (100%) create mode 100644 packages/customer/src/types/services/customer-group-customer.ts create mode 100644 packages/customer/src/types/services/index.ts delete mode 100644 packages/payment/src/services/payment-collection.ts delete mode 100644 packages/payment/src/types/repositories.ts delete mode 100644 packages/pricing/src/services/currency.ts delete mode 100644 packages/pricing/src/services/money-amount.ts delete mode 100644 packages/pricing/src/services/price-set-money-amount-rules.ts delete mode 100644 packages/pricing/src/services/price-set-money-amount.ts delete mode 100644 packages/pricing/src/services/price-set-rule-type.ts delete mode 100644 packages/pricing/src/services/price-set.ts delete mode 100644 packages/product/src/services/product-image.ts delete mode 100644 packages/product/src/services/product-option-value.ts delete mode 100644 packages/product/src/types/repositories.ts delete mode 100644 packages/promotion/src/services/application-method.ts delete mode 100644 packages/promotion/src/services/campaign-budget.ts delete mode 100644 packages/promotion/src/services/campaign.ts delete mode 100644 packages/promotion/src/services/promotion-rule-value.ts delete mode 100644 packages/promotion/src/services/promotion-rule.ts delete mode 100644 packages/promotion/src/services/promotion.ts delete mode 100644 packages/promotion/src/types/repositories.ts delete mode 100644 packages/sales-channel/src/services/sales-channel.ts delete mode 100644 packages/sales-channel/src/types/repositories.ts create mode 100644 packages/types/src/modules-sdk/internal-module-service.ts create mode 100644 packages/types/src/modules-sdk/module-service.ts create mode 100644 packages/utils/src/common/__tests__/pluralize.spec.ts create mode 100644 packages/utils/src/common/plurailze.ts delete mode 100644 packages/utils/src/dal/repository.ts create mode 100644 packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts create mode 100644 packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts create mode 100644 packages/utils/src/modules-sdk/abstract-module-service-factory.ts delete mode 100644 packages/utils/src/modules-sdk/abstract-service-factory.ts create mode 100644 packages/utils/src/modules-sdk/internal-module-service-factory.ts delete mode 100644 packages/workflow-engine-inmemory/src/services/workflow-execution.ts delete mode 100644 packages/workflow-engine-redis/src/services/workflow-execution.ts diff --git a/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts index abaf5abe24ca1..ca4753eac7b43 100644 --- a/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts +++ b/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts @@ -9,6 +9,8 @@ import { createAuthenticatedCustomer } from "../../../helpers/create-authenticat const env = { MEDUSA_FF_MEDUSA_V2: true } +jest.setTimeout(100000) + describe("GET /store/customers/me/addresses", () => { let dbConnection let appContainer @@ -16,13 +18,17 @@ describe("GET /store/customers/me/addresses", () => { let customerModuleService: ICustomerModuleService beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) - dbConnection = await initDb({ cwd, env } as any) - shutdownServer = await startBootstrapApp({ cwd, env }) - appContainer = getContainer() - customerModuleService = appContainer.resolve( - ModuleRegistrationName.CUSTOMER - ) + try { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + } catch (error) { + console.error(error) + } }) afterAll(async () => { diff --git a/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts b/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts index 51e99ad6ad58d..3fd7165c48d5e 100644 --- a/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts @@ -5,14 +5,13 @@ import { simpleProductFactory, simpleRegionFactory, } from "../../../../factories" - -import { PricingModuleService } from "@medusajs/pricing" -import { ProductModuleService } from "@medusajs/product" import { AxiosInstance } from "axios" import path from "path" import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import adminSeeder from "../../../../helpers/admin-seeder" import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { ProductModuleService } from "@medusajs/product" +import { PricingModuleService } from "@medusajs/pricing" jest.setTimeout(50000) diff --git a/integration-tests/plugins/__tests__/workflows/product/create-product.ts b/integration-tests/plugins/__tests__/workflows/product/create-product.ts index 5d218d108e7a8..11de759e379cc 100644 --- a/integration-tests/plugins/__tests__/workflows/product/create-product.ts +++ b/integration-tests/plugins/__tests__/workflows/product/create-product.ts @@ -11,7 +11,7 @@ import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" -jest.setTimeout(30000) +jest.setTimeout(50000) describe("CreateProduct workflow", function () { let medusaContainer @@ -129,7 +129,7 @@ describe("CreateProduct workflow", function () { expect(product).toEqual( expect.objectContaining({ - deleted_at: expect.any(String), + deleted_at: expect.any(Date), }) ) }) diff --git a/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts b/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts index 934bf94ec7d68..3ce97ecd458e4 100644 --- a/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts @@ -1,16 +1,16 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" -import { AuthProviderService } from "@services" import { MikroOrmWrapper } from "../../../utils" import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createMedusaContainer } from "@medusajs/utils" import { asValue } from "awilix" import ContainerLoader from "../../../../src/loaders/container" +import { ModulesSdkTypes } from "@medusajs/types" jest.setTimeout(30000) describe("AuthProvider Service", () => { - let service: AuthProviderService + let service: ModulesSdkTypes.InternalModuleService let testManager: SqlEntityManager let repositoryManager: SqlEntityManager @@ -180,7 +180,7 @@ describe("AuthProvider Service", () => { error = e } - expect(error.message).toEqual('"authProviderProvider" must be defined') + expect(error.message).toEqual("authProvider - provider must be defined") }) it("should return authProvider based on config select param", async () => { diff --git a/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts b/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts index 1737aa19b4f97..7dd4afe52e3b0 100644 --- a/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts @@ -166,7 +166,7 @@ describe("AuthUser Service", () => { error = e } - expect(error.message).toEqual('"authUserId" must be defined') + expect(error.message).toEqual("authUser - id must be defined") }) }) @@ -229,7 +229,7 @@ describe("AuthUser Service", () => { id: "test", provider_id: "manual", entity_id: "test", - scope: "store" + scope: "store", }, ]) diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts index 2fdf373176674..37aaa50ad296d 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts @@ -43,9 +43,8 @@ describe("AuthModuleService - AuthProvider", () => { describe("listAuthProviders", () => { it("should list AuthProviders", async () => { const authProviders = await service.listAuthProviders() - const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(serialized).toEqual( + expect(authProviders).toEqual( expect.arrayContaining([ expect.objectContaining({ provider: "manual", @@ -80,9 +79,7 @@ describe("AuthModuleService - AuthProvider", () => { is_active: true, }) - const serialized = JSON.parse(JSON.stringify(authProviders)) - - expect(serialized).toEqual([ + expect(authProviders).toEqual([ expect.objectContaining({ provider: "manual", }), @@ -99,10 +96,9 @@ describe("AuthModuleService - AuthProvider", () => { describe("listAndCountAuthProviders", () => { it("should list and count AuthProviders", async () => { const [authProviders, count] = await service.listAndCountAuthProviders() - const serialized = JSON.parse(JSON.stringify(authProviders)) expect(count).toEqual(4) - expect(serialized).toEqual([ + expect(authProviders).toEqual([ expect.objectContaining({ provider: "manual", }), @@ -136,10 +132,8 @@ describe("AuthModuleService - AuthProvider", () => { is_active: true, }) - const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(count).toEqual(3) - expect(serialized).toEqual([ + expect(authProviders).toEqual([ expect.objectContaining({ provider: "manual", }), @@ -171,9 +165,7 @@ describe("AuthModuleService - AuthProvider", () => { select: ["provider"], }) - const serialized = JSON.parse(JSON.stringify(authProvider)) - - expect(serialized).toEqual({ + expect(authProvider).toEqual({ provider, }) }) @@ -201,7 +193,7 @@ describe("AuthModuleService - AuthProvider", () => { error = e } - expect(error.message).toEqual('"authProviderProvider" must be defined') + expect(error.message).toEqual("authProvider - provider must be defined") }) }) @@ -209,7 +201,7 @@ describe("AuthModuleService - AuthProvider", () => { const provider = "manual" it("should delete the authProviders given a provider successfully", async () => { - await service.deleteAuthProvider([provider]) + await service.deleteAuthProviders([provider]) const authProviders = await service.listAuthProviders({ provider: [provider], diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts index 641ee17e1dd33..3ca27ed83e01d 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts @@ -43,17 +43,16 @@ describe("AuthModuleService - AuthUser", () => { describe("listAuthUsers", () => { it("should list authUsers", async () => { const authUsers = await service.listAuthUsers() - const serialized = JSON.parse(JSON.stringify(authUsers)) - expect(serialized).toEqual([ + expect(authUsers).toEqual([ expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "store", + provider: { provider: "store" }, }), ]) }) @@ -75,9 +74,7 @@ describe("AuthModuleService - AuthUser", () => { provider_id: "manual", }) - const serialized = JSON.parse(JSON.stringify(authUsers)) - - expect(serialized).toEqual([ + expect(authUsers).toEqual([ expect.objectContaining({ id: "test-id", }), @@ -91,18 +88,17 @@ describe("AuthModuleService - AuthUser", () => { describe("listAndCountAuthUsers", () => { it("should list and count authUsers", async () => { const [authUsers, count] = await service.listAndCountAuthUsers() - const serialized = JSON.parse(JSON.stringify(authUsers)) expect(count).toEqual(3) - expect(serialized).toEqual([ + expect(authUsers).toEqual([ expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "store", + provider: { provider: "store" }, }), ]) }) @@ -171,7 +167,7 @@ describe("AuthModuleService - AuthUser", () => { error = e } - expect(error.message).toEqual('"authUserId" must be defined') + expect(error.message).toEqual("authUser - id must be defined") }) it("should return authUser based on config select param", async () => { @@ -179,9 +175,7 @@ describe("AuthModuleService - AuthUser", () => { select: ["id"], }) - const serialized = JSON.parse(JSON.stringify(authUser)) - - expect(serialized).toEqual({ + expect(authUser).toEqual({ id, }) }) @@ -191,7 +185,7 @@ describe("AuthModuleService - AuthUser", () => { const id = "test-id" it("should delete the authUsers given an id successfully", async () => { - await service.deleteAuthUser([id]) + await service.deleteAuthUsers([id]) const authUsers = await service.listAuthUsers({ id: [id], diff --git a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts index f3b79046f4d66..b118e54ac026e 100644 --- a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts @@ -45,9 +45,8 @@ describe("AuthModuleService - AuthProvider", () => { describe("listAuthProviders", () => { it("should list default AuthProviders registered by loaders", async () => { const authProviders = await service.listAuthProviders() - const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(serialized).toEqual( + expect(authProviders).toEqual( expect.arrayContaining([ expect.objectContaining({ provider: "emailpass", diff --git a/packages/auth/src/providers/google.ts b/packages/auth/src/providers/google.ts index b5558216e1240..77f7d9d8788eb 100644 --- a/packages/auth/src/providers/google.ts +++ b/packages/auth/src/providers/google.ts @@ -1,10 +1,11 @@ import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils" import { - AuthProviderScope, AuthenticationInput, AuthenticationResponse, + AuthProviderScope, + ModulesSdkTypes, } from "@medusajs/types" -import { AuthProviderService, AuthUserService } from "@services" +import { AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" import { AuthorizationCode } from "simple-oauth2" @@ -12,7 +13,7 @@ import url from "url" type InjectedDependencies = { authUserService: AuthUserService - authProviderService: AuthProviderService + authProviderService: ModulesSdkTypes.InternalModuleService } type ProviderConfig = { @@ -25,13 +26,13 @@ class GoogleProvider extends AbstractAuthModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" - protected readonly authUserSerivce_: AuthUserService - protected readonly authProviderService_: AuthProviderService + protected readonly authUserService_: AuthUserService + protected readonly authProviderService_: ModulesSdkTypes.InternalModuleService constructor({ authUserService, authProviderService }: InjectedDependencies) { super(arguments[0]) - this.authUserSerivce_ = authUserService + this.authUserService_ = authUserService this.authProviderService_ = authProviderService } @@ -89,13 +90,13 @@ class GoogleProvider extends AbstractAuthModuleProvider { let authUser try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + authUser = await this.authUserService_.retrieveByProviderAndEntityId( entity_id, GoogleProvider.PROVIDER ) } catch (error) { if (error.type === MedusaError.Types.NOT_FOUND) { - const [createdAuthUser] = await this.authUserSerivce_.create([ + const [createdAuthUser] = await this.authUserService_.create([ { entity_id, provider: GoogleProvider.PROVIDER, diff --git a/packages/auth/src/services/auth-module.ts b/packages/auth/src/services/auth-module.ts index 34ac05f0dda7d..995ec3b734dc4 100644 --- a/packages/auth/src/services/auth-module.ts +++ b/packages/auth/src/services/auth-module.ts @@ -1,35 +1,35 @@ import jwt from "jsonwebtoken" import { + AuthenticationInput, + AuthenticationResponse, AuthProviderDTO, AuthTypes, AuthUserDTO, - AuthenticationInput, - AuthenticationResponse, Context, CreateAuthProviderDTO, CreateAuthUserDTO, DAL, - FilterableAuthProviderProps, - FilterableAuthUserProps, - FindConfig, InternalModuleDeclaration, JWTGenerationOptions, - MedusaContainer, ModuleJoinerConfig, + ModulesSdkTypes, UpdateAuthUserDTO, } from "@medusajs/types" + +import { AuthProvider, AuthUser } from "@models" + +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" + import { AbstractAuthModuleProvider, InjectManager, InjectTransactionManager, MedusaContext, MedusaError, + ModulesSdkUtils, } from "@medusajs/utils" -import { AuthProvider, AuthUser } from "@models" -import { AuthProviderService, AuthUserService } from "@services" import { ServiceTypes } from "@types" -import { joinerConfig } from "../joiner-config" type AuthModuleOptions = { jwt_secret: string @@ -42,28 +42,32 @@ type AuthJWTPayload = { type InjectedDependencies = { baseRepository: DAL.RepositoryService - authUserService: AuthUserService - authProviderService: AuthProviderService + authUserService: ModulesSdkTypes.InternalModuleService + authProviderService: ModulesSdkTypes.InternalModuleService } +const generateMethodForModels = [AuthProvider, AuthUser] + export default class AuthModuleService< - TAuthUser extends AuthUser = AuthUser, - TAuthProvider extends AuthProvider = AuthProvider -> implements AuthTypes.IAuthModuleService + TAuthUser extends AuthUser = AuthUser, + TAuthProvider extends AuthProvider = AuthProvider + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + AuthTypes.AuthProviderDTO, + { + AuthUser: { dto: AuthUserDTO } + AuthProvider: { dto: AuthProviderDTO } + } + >(AuthProvider, generateMethodForModels, entityNameToLinkableKeysMap) + implements AuthTypes.IAuthModuleService { - __joinerConfig(): ModuleJoinerConfig { - return joinerConfig - } - __hooks = { onApplicationStart: async () => await this.createProvidersOnLoad(), } - - protected __container__: MedusaContainer protected baseRepository_: DAL.RepositoryService - - protected authUserService_: AuthUserService - protected authProviderService_: AuthProviderService + protected authUserService_: ModulesSdkTypes.InternalModuleService + protected authProviderService_: ModulesSdkTypes.InternalModuleService protected options_: AuthModuleOptions constructor( @@ -75,66 +79,17 @@ export default class AuthModuleService< options: AuthModuleOptions, protected readonly moduleDeclaration: InternalModuleDeclaration ) { - this.__container__ = arguments[0] + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.authUserService_ = authUserService this.authProviderService_ = authProviderService this.options_ = options } - async retrieveAuthProvider( - provider: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authProvider = await this.authProviderService_.retrieve( - provider, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authProvider, - { populate: true } - ) - } - - async listAuthProviders( - filters: FilterableAuthProviderProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authProviders = await this.authProviderService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authProviders, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCountAuthProviders( - filters: FilterableAuthProviderProps = {}, - config: FindConfig, - @MedusaContext() sharedContext: Context = {} - ): Promise<[AuthTypes.AuthProviderDTO[], number]> { - const [authProviders, count] = await this.authProviderService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - authProviders, - { populate: true } - ), - count, - ] + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig } async generateJwtToken( @@ -205,18 +160,11 @@ export default class AuthModuleService< return Array.isArray(data) ? serializedProviders : serializedProviders[0] } - @InjectTransactionManager("baseRepository_") - protected async createAuthProviders_( - data: any[], - @MedusaContext() sharedContext: Context - ): Promise { - return await this.authProviderService_.create(data, sharedContext) - } - updateAuthProvider( data: AuthTypes.UpdateAuthProviderDTO[], sharedContext?: Context ): Promise + updateAuthProvider( data: AuthTypes.UpdateAuthProviderDTO, sharedContext?: Context @@ -247,78 +195,11 @@ export default class AuthModuleService< return await this.authProviderService_.update(data, sharedContext) } - @InjectTransactionManager("baseRepository_") - async deleteAuthProvider( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.authProviderService_.delete(ids, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveAuthUser( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authUser = await this.authUserService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authUser, - { - exclude: ["password_hash"], - } - ) - } - - @InjectManager("baseRepository_") - async listAuthUsers( - filters: FilterableAuthUserProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authUsers = await this.authUserService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authUsers, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountAuthUsers( - filters: FilterableAuthUserProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[AuthUserDTO[], number]> { - const [authUsers, count] = await this.authUserService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize(authUsers, { - populate: true, - }), - count, - ] - } - createAuthUser( data: CreateAuthUserDTO[], sharedContext?: Context ): Promise + createAuthUser( data: CreateAuthUserDTO, sharedContext?: Context @@ -342,23 +223,17 @@ export default class AuthModuleService< return Array.isArray(data) ? serializedUsers : serializedUsers[0] } - @InjectTransactionManager("baseRepository_") - protected async createAuthUsers_( - data: CreateAuthUserDTO[], - @MedusaContext() sharedContext: Context - ): Promise { - return await this.authUserService_.create(data, sharedContext) - } - updateAuthUser( data: UpdateAuthUserDTO[], sharedContext?: Context ): Promise + updateAuthUser( data: UpdateAuthUserDTO, sharedContext?: Context ): Promise + // TODO: should be pluralized, see convention about the methods naming or the abstract module service interface definition @engineering @InjectManager("baseRepository_") async updateAuthUser( data: UpdateAuthUserDTO | UpdateAuthUserDTO[], @@ -385,14 +260,6 @@ export default class AuthModuleService< return await this.authUserService_.update(data, sharedContext) } - @InjectTransactionManager("baseRepository_") - async deleteAuthUser( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.authUserService_.delete(ids, sharedContext) - } - protected getRegisteredAuthenticationProvider( provider: string, { authScope }: AuthenticationInput @@ -448,6 +315,22 @@ export default class AuthModuleService< } } + @InjectTransactionManager("baseRepository_") + protected async createAuthProviders_( + data: any[], + @MedusaContext() sharedContext: Context + ): Promise { + return await this.authProviderService_.create(data, sharedContext) + } + + @InjectTransactionManager("baseRepository_") + protected async createAuthUsers_( + data: CreateAuthUserDTO[], + @MedusaContext() sharedContext: Context + ): Promise { + return await this.authUserService_.create(data, sharedContext) + } + private async createProvidersOnLoad() { const providersToLoad = this.__container__["auth_providers"] diff --git a/packages/auth/src/services/auth-provider.ts b/packages/auth/src/services/auth-provider.ts deleted file mode 100644 index 241fb56e800df..0000000000000 --- a/packages/auth/src/services/auth-provider.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { AuthProvider } from "@models" - -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - authProviderRepository: DAL.RepositoryService -} - -export default class AuthProviderService< - TEntity extends AuthProvider = AuthProvider -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateAuthProviderDTO - update: ServiceTypes.UpdateAuthProviderDTO - } ->(AuthProvider) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/auth/src/services/auth-user.ts b/packages/auth/src/services/auth-user.ts index 7625415da9360..a8e6fcc264df4 100644 --- a/packages/auth/src/services/auth-user.ts +++ b/packages/auth/src/services/auth-user.ts @@ -1,4 +1,10 @@ -import { AuthTypes, Context, DAL, FindConfig } from "@medusajs/types" +import { + AuthTypes, + Context, + DAL, + FindConfig, + RepositoryService, +} from "@medusajs/types" import { InjectManager, MedusaContext, @@ -6,7 +12,6 @@ import { ModulesSdkUtils, } from "@medusajs/utils" import { AuthUser } from "@models" -import { ServiceTypes, RepositoryTypes } from "@types" type InjectedDependencies = { authUserRepository: DAL.RepositoryService @@ -14,13 +19,11 @@ type InjectedDependencies = { export default class AuthUserService< TEntity extends AuthUser = AuthUser -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateAuthUserDTO - } ->(AuthUser) { - protected readonly authUserRepository_: RepositoryTypes.IAuthUserRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + AuthUser +) { + protected readonly authUserRepository_: RepositoryService + constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) @@ -28,9 +31,7 @@ export default class AuthUserService< } @InjectManager("authUserRepository_") - async retrieveByProviderAndEntityId< - TEntityMethod = AuthTypes.AuthUserDTO - >( + async retrieveByProviderAndEntityId( entityId: string, provider: string, config: FindConfig = {}, diff --git a/packages/auth/src/services/index.ts b/packages/auth/src/services/index.ts index 03a3c0933defc..547d1a5466ca0 100644 --- a/packages/auth/src/services/index.ts +++ b/packages/auth/src/services/index.ts @@ -1,3 +1,2 @@ export { default as AuthModuleService } from "./auth-module" -export { default as AuthProviderService } from "./auth-provider" export { default as AuthUserService } from "./auth-user" diff --git a/packages/auth/src/types/repositories/index.ts b/packages/auth/src/types/repositories/index.ts index 86ed73825729d..b4282c985c6a3 100644 --- a/packages/auth/src/types/repositories/index.ts +++ b/packages/auth/src/types/repositories/index.ts @@ -1,28 +1,2 @@ -import { AuthProvider, AuthUser } from "@models" -import { CreateAuthProviderDTO, UpdateAuthProviderDTO } from "./auth-provider" -import { DAL } from "@medusajs/types" -import { CreateAuthUserDTO, UpdateAuthUserDTO } from "./auth-user" - export * from "./auth-user" export * from "./auth-provider" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IAuthProviderRepository< - TEntity extends AuthProvider = AuthProvider -> extends DAL.RepositoryService< - TEntity, - { - create: CreateAuthProviderDTO - update: UpdateAuthProviderDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IAuthUserRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateAuthUserDTO - update: UpdateAuthUserDTO - } - > {} diff --git a/packages/cart/src/services/address.ts b/packages/cart/src/services/address.ts deleted file mode 100644 index 383a07707bc37..0000000000000 --- a/packages/cart/src/services/address.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Address } from "@models" -import { CreateAddressDTO, UpdateAddressDTO } from "@types" - -type InjectedDependencies = { - addressRepository: DAL.RepositoryService -} - -export default class AddressService< - TEntity extends Address = Address -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateAddressDTO - update: UpdateAddressDTO - } ->(Address) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index e3eaff368aa6f..79274eed6b21d 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -3,20 +3,22 @@ import { Context, DAL, FilterableLineItemTaxLineProps, - FindConfig, ICartModuleService, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, } from "@medusajs/types" import { InjectManager, InjectTransactionManager, - MedusaContext, - MedusaError, isObject, isString, + MedusaContext, + MedusaError, + ModulesSdkUtils, } from "@medusajs/utils" import { + Address, Cart, LineItem, LineItemAdjustment, @@ -25,32 +27,73 @@ import { ShippingMethodAdjustment, ShippingMethodTaxLine, } from "@models" -import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" -import { joinerConfig } from "../joiner-config" -import * as services from "../services" +import { + CreateLineItemDTO, + CreateLineItemTaxLineDTO, + CreateShippingMethodDTO, + CreateShippingMethodTaxLineDTO, + UpdateLineItemDTO, + UpdateLineItemTaxLineDTO, + UpdateShippingMethodTaxLineDTO, +} from "@types" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - cartService: services.CartService - addressService: services.AddressService - lineItemService: services.LineItemService - shippingMethodAdjustmentService: services.ShippingMethodAdjustmentService - shippingMethodService: services.ShippingMethodService - lineItemAdjustmentService: services.LineItemAdjustmentService - lineItemTaxLineService: services.LineItemTaxLineService - shippingMethodTaxLineService: services.ShippingMethodTaxLineService + cartService: ModulesSdkTypes.InternalModuleService + addressService: ModulesSdkTypes.InternalModuleService + lineItemService: ModulesSdkTypes.InternalModuleService + shippingMethodAdjustmentService: ModulesSdkTypes.InternalModuleService + shippingMethodService: ModulesSdkTypes.InternalModuleService + lineItemAdjustmentService: ModulesSdkTypes.InternalModuleService + lineItemTaxLineService: ModulesSdkTypes.InternalModuleService + shippingMethodTaxLineService: ModulesSdkTypes.InternalModuleService } -export default class CartModuleService implements ICartModuleService { +const generateMethodForModels = [ + Address, + LineItem, + LineItemAdjustment, + LineItemTaxLine, + ShippingMethod, + ShippingMethodAdjustment, + ShippingMethodTaxLine, +] + +export default class CartModuleService< + TCart extends Cart = Cart, + TAddress extends Address = Address, + TLineItem extends LineItem = LineItem, + TLineItemAdjustment extends LineItemAdjustment = LineItemAdjustment, + TLineItemTaxLine extends LineItemTaxLine = LineItemTaxLine, + TShippingMethodAdjustment extends ShippingMethodAdjustment = ShippingMethodAdjustment, + TShippingMethodTaxLine extends ShippingMethodTaxLine = ShippingMethodTaxLine, + TShippingMethod extends ShippingMethod = ShippingMethod + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + CartTypes.CartDTO, + { + Address: { dto: CartTypes.CartAddressDTO } + LineItem: { dto: CartTypes.CartLineItemDTO } + LineItemAdjustment: { dto: CartTypes.LineItemAdjustmentDTO } + LineItemTaxLine: { dto: CartTypes.LineItemTaxLineDTO } + ShippingMethod: { dto: CartTypes.CartShippingMethodDTO } + ShippingMethodAdjustment: { dto: CartTypes.ShippingMethodAdjustmentDTO } + ShippingMethodTaxLine: { dto: CartTypes.ShippingMethodTaxLineDTO } + } + >(Cart, generateMethodForModels, entityNameToLinkableKeysMap) + implements ICartModuleService +{ protected baseRepository_: DAL.RepositoryService - protected cartService_: services.CartService - protected addressService_: services.AddressService - protected lineItemService_: services.LineItemService - protected shippingMethodAdjustmentService_: services.ShippingMethodAdjustmentService - protected shippingMethodService_: services.ShippingMethodService - protected lineItemAdjustmentService_: services.LineItemAdjustmentService - protected lineItemTaxLineService_: services.LineItemTaxLineService - protected shippingMethodTaxLineService_: services.ShippingMethodTaxLineService + protected cartService_: ModulesSdkTypes.InternalModuleService + protected addressService_: ModulesSdkTypes.InternalModuleService + protected lineItemService_: ModulesSdkTypes.InternalModuleService + protected shippingMethodAdjustmentService_: ModulesSdkTypes.InternalModuleService + protected shippingMethodService_: ModulesSdkTypes.InternalModuleService + protected lineItemAdjustmentService_: ModulesSdkTypes.InternalModuleService + protected lineItemTaxLineService_: ModulesSdkTypes.InternalModuleService + protected shippingMethodTaxLineService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -66,6 +109,9 @@ export default class CartModuleService implements ICartModuleService { }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.cartService_ = cartService this.addressService_ = addressService @@ -81,52 +127,6 @@ export default class CartModuleService implements ICartModuleService { return joinerConfig } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const cart = await this.cartService_.retrieve(id, config, sharedContext) - - return await this.baseRepository_.serialize(cart, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async list( - filters: CartTypes.FilterableCartProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const carts = await this.cartService_.list(filters, config, sharedContext) - - return this.baseRepository_.serialize(carts, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: CartTypes.FilterableCartProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CartTypes.CartDTO[], number]> { - const [carts, count] = await this.cartService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize(carts, { - populate: true, - }), - count, - ] - } - async create( data: CartTypes.CreateCartDTO[], sharedContext?: Context @@ -229,98 +229,6 @@ 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 - - @InjectTransactionManager("baseRepository_") - async delete( - ids: string[] | string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const cartIds = Array.isArray(ids) ? ids : [ids] - await this.cartService_.delete(cartIds, sharedContext) - } - - @InjectManager("baseRepository_") - async listAddresses( - filters: CartTypes.FilterableAddressProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const addresses = await this.addressService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - addresses, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async retrieveLineItem( - itemId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const item = await this.lineItemService_.retrieve( - itemId, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - item, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listLineItems( - filters: CartTypes.FilterableLineItemProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const items = await this.lineItemService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - items, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listShippingMethods( - filters: CartTypes.FilterableShippingMethodProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const methods = await this.shippingMethodService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.CartShippingMethodDTO[] - >(methods, { - populate: true, - }) - } - addLineItems( data: CartTypes.CreateLineItemForCartDTO ): Promise @@ -585,18 +493,6 @@ export default class CartModuleService implements ICartModuleService { return await this.addressService_.update(data, sharedContext) } - async deleteAddresses(ids: string[], sharedContext?: Context): Promise - async deleteAddresses(ids: string, sharedContext?: Context): Promise - - @InjectTransactionManager("baseRepository_") - async deleteAddresses( - ids: string[] | string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const addressIds = Array.isArray(ids) ? ids : [ids] - await this.addressService_.delete(addressIds, sharedContext) - } - async addShippingMethods( data: CartTypes.CreateShippingMethodDTO ): Promise @@ -665,7 +561,10 @@ export default class CartModuleService implements ICartModuleService { data: CartTypes.CreateShippingMethodDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { - return await this.shippingMethodService_.create(data, sharedContext) + return await this.shippingMethodService_.create( + data as unknown as CreateShippingMethodDTO[], + sharedContext + ) } async removeShippingMethods( @@ -708,25 +607,6 @@ 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 @@ -882,25 +762,6 @@ export default class CartModuleService implements ICartModuleService { await this.lineItemAdjustmentService_.delete(ids, sharedContext) } - @InjectManager("baseRepository_") - async listShippingMethodAdjustments( - filters: CartTypes.FilterableShippingMethodAdjustmentProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const adjustments = await this.shippingMethodAdjustmentService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.ShippingMethodAdjustmentDTO[] - >(adjustments, { - populate: true, - }) - } - @InjectTransactionManager("baseRepository_") async setShippingMethodAdjustments( cartId: string, @@ -1070,26 +931,6 @@ export default class CartModuleService implements ICartModuleService { await this.shippingMethodAdjustmentService_.delete(ids, sharedContext) } - @InjectManager("baseRepository_") - async listLineItemTaxLines( - filters: CartTypes.FilterableLineItemTaxLineProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const taxLines = await this.lineItemTaxLineService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - taxLines, - { - populate: true, - } - ) - } - addLineItemTaxLines( taxLines: CartTypes.CreateLineItemTaxLineDTO[] ): Promise @@ -1123,14 +964,14 @@ export default class CartModuleService implements ICartModuleService { const lines = Array.isArray(taxLines) ? taxLines : [taxLines] addedTaxLines = await this.lineItemTaxLineService_.create( - lines as CartTypes.CreateLineItemTaxLineDTO[], + lines as CreateLineItemTaxLineDTO[], sharedContext ) } else { const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData] addedTaxLines = await this.lineItemTaxLineService_.create( - data as CartTypes.CreateLineItemTaxLineDTO[], + data as CreateLineItemTaxLineDTO[], sharedContext ) } @@ -1184,13 +1025,15 @@ export default class CartModuleService implements ICartModuleService { } }) - await this.lineItemTaxLineService_.delete( - toDelete.map((taxLine) => taxLine!.id), - sharedContext - ) + if (toDelete.length) { + await this.lineItemTaxLineService_.delete( + toDelete.map((taxLine) => taxLine!.id), + sharedContext + ) + } const result = await this.lineItemTaxLineService_.upsert( - taxLines, + taxLines as UpdateLineItemTaxLineDTO[], sharedContext ) @@ -1242,25 +1085,6 @@ export default class CartModuleService implements ICartModuleService { await this.lineItemTaxLineService_.delete(ids, sharedContext) } - @InjectManager("baseRepository_") - async listShippingMethodTaxLines( - filters: CartTypes.FilterableShippingMethodTaxLineProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const taxLines = await this.shippingMethodTaxLineService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.ShippingMethodTaxLineDTO[] - >(taxLines, { - populate: true, - }) - } - addShippingMethodTaxLines( taxLines: CartTypes.CreateShippingMethodTaxLineDTO[] ): Promise @@ -1296,12 +1120,12 @@ export default class CartModuleService implements ICartModuleService { const lines = Array.isArray(taxLines) ? taxLines : [taxLines] addedTaxLines = await this.shippingMethodTaxLineService_.create( - lines as CartTypes.CreateShippingMethodTaxLineDTO[], + lines as CreateShippingMethodTaxLineDTO[], sharedContext ) } else { addedTaxLines = await this.shippingMethodTaxLineService_.create( - taxLines as CartTypes.CreateShippingMethodTaxLineDTO[], + taxLines as CreateShippingMethodTaxLineDTO[], sharedContext ) } @@ -1367,7 +1191,7 @@ export default class CartModuleService implements ICartModuleService { } const result = await this.shippingMethodTaxLineService_.upsert( - taxLines, + taxLines as UpdateShippingMethodTaxLineDTO[], sharedContext ) diff --git a/packages/cart/src/services/cart.ts b/packages/cart/src/services/cart.ts deleted file mode 100644 index a9c594555b1ca..0000000000000 --- a/packages/cart/src/services/cart.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Cart } from "@models" -import { CreateCartDTO, UpdateCartDTO } from "@types" - -type InjectedDependencies = { - cartRepository: DAL.RepositoryService -} - -export default class CartService< - TEntity extends Cart = Cart -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCartDTO - update: UpdateCartDTO - } ->(Cart) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/index.ts b/packages/cart/src/services/index.ts index 7120b701d7fee..2ed2053ffcb36 100644 --- a/packages/cart/src/services/index.ts +++ b/packages/cart/src/services/index.ts @@ -1,10 +1 @@ -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 LineItemTaxLineService } from "./line-item-tax-line" -export { default as ShippingMethodService } from "./shipping-method" -export { default as ShippingMethodAdjustmentService } from "./shipping-method-adjustment" -export { default as ShippingMethodTaxLineService } from "./shipping-method-tax-line" - diff --git a/packages/cart/src/services/line-item-adjustment.ts b/packages/cart/src/services/line-item-adjustment.ts deleted file mode 100644 index 9ee93b2d95820..0000000000000 --- a/packages/cart/src/services/line-item-adjustment.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/services/line-item-tax-line.ts b/packages/cart/src/services/line-item-tax-line.ts deleted file mode 100644 index a740fae4366d1..0000000000000 --- a/packages/cart/src/services/line-item-tax-line.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - CreateLineItemTaxLineDTO, - DAL, - UpdateLineItemTaxLineDTO, -} from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { LineItemTaxLine } from "@models" - -type InjectedDependencies = { - lineItemTaxLineRepository: DAL.RepositoryService -} - -export default class LineItemTaxLineService< - TEntity extends LineItemTaxLine = LineItemTaxLine -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateLineItemTaxLineDTO - update: UpdateLineItemTaxLineDTO - } ->(LineItemTaxLine) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/line-item.ts b/packages/cart/src/services/line-item.ts deleted file mode 100644 index ec736f4010249..0000000000000 --- a/packages/cart/src/services/line-item.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { LineItem } from "@models" -import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" - -type InjectedDependencies = { - lineItemRepository: DAL.RepositoryService -} - -export default class LineItemService< - TEntity extends LineItem = LineItem -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateLineItemDTO - update: UpdateLineItemDTO - } ->(LineItem) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/shipping-method-adjustment.ts b/packages/cart/src/services/shipping-method-adjustment.ts deleted file mode 100644 index 5688cd662babe..0000000000000 --- a/packages/cart/src/services/shipping-method-adjustment.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ShippingMethodAdjustment } from "@models" -import { - CreateShippingMethodAdjustmentDTO, - UpdateShippingMethodAdjustmentDTO, -} from "@types" - -type InjectedDependencies = { - shippingMethodAdjustmentRepository: DAL.RepositoryService -} - -export default class ShippingMethodAdjustmentService< - TEntity extends ShippingMethodAdjustment = ShippingMethodAdjustment -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateShippingMethodAdjustmentDTO - update: UpdateShippingMethodAdjustmentDTO - } ->(ShippingMethodAdjustment) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/shipping-method-tax-line.ts b/packages/cart/src/services/shipping-method-tax-line.ts deleted file mode 100644 index 9229d6556717b..0000000000000 --- a/packages/cart/src/services/shipping-method-tax-line.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CreateShippingMethodTaxLineDTO, DAL, UpdateShippingMethodTaxLineDTO } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ShippingMethodTaxLine } from "@models" - -type InjectedDependencies = { - shippingMethodTaxLineRepository: DAL.RepositoryService -} - -export default class ShippingMethodTaxLineService< - TEntity extends ShippingMethodTaxLine = ShippingMethodTaxLine -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateShippingMethodTaxLineDTO - update: UpdateShippingMethodTaxLineDTO - } ->(ShippingMethodTaxLine) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/shipping-method.ts b/packages/cart/src/services/shipping-method.ts deleted file mode 100644 index f3cf6671442ee..0000000000000 --- a/packages/cart/src/services/shipping-method.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ShippingMethod } from "@models" -import { CreateShippingMethodDTO, UpdateShippingMethodDTO } from "../types" - -type InjectedDependencies = { - shippingMethodRepository: DAL.RepositoryService -} - -export default class ShippingMethodService< - TEntity extends ShippingMethod = ShippingMethod -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateShippingMethodDTO - update: UpdateShippingMethodDTO - } ->(ShippingMethod) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/types/index.ts b/packages/cart/src/types/index.ts index 03f39f0916b24..9e420fcb7f908 100644 --- a/packages/cart/src/types/index.ts +++ b/packages/cart/src/types/index.ts @@ -5,7 +5,6 @@ export * from "./cart" export * from "./line-item" export * from "./line-item-adjustment" export * from "./line-item-tax-line" -export * from "./repositories" export * from "./shipping-method" export * from "./shipping-method-adjustment" export * from "./shipping-method-tax-line" diff --git a/packages/cart/src/types/repositories.ts b/packages/cart/src/types/repositories.ts deleted file mode 100644 index 3d028f6c8e20a..0000000000000 --- a/packages/cart/src/types/repositories.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { DAL } from "@medusajs/types" -import { - Cart, - LineItem, - LineItemAdjustment, - LineItemTaxLine, - ShippingMethod, - ShippingMethodAdjustment, - ShippingMethodTaxLine, -} 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 { - CreateLineItemTaxLineDTO, - UpdateLineItemTaxLineDTO, -} from "./line-item-tax-line" -import { - CreateShippingMethodDTO, - UpdateShippingMethodDTO, -} from "./shipping-method" -import { - CreateShippingMethodAdjustmentDTO, - UpdateShippingMethodAdjustmentDTO, -} from "./shipping-method-adjustment" -import { - CreateShippingMethodTaxLineDTO, - UpdateShippingMethodTaxLineDTO, -} from "./shipping-method-tax-line" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IAddressRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateAddressDTO - update: UpdateAddressDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICartRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateCartDTO - update: UpdateCartDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ILineItemRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateLineItemDTO - update: UpdateLineItemDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IShippingMethodRepository< - TEntity extends ShippingMethod = ShippingMethod -> extends DAL.RepositoryService< - TEntity, - { - create: CreateShippingMethodDTO - update: UpdateShippingMethodDTO - } - > {} - -export interface ILineItemAdjustmentRepository< - TEntity extends LineItemAdjustment = LineItemAdjustment -> extends DAL.RepositoryService< - TEntity, - { - create: CreateLineItemAdjustmentDTO - update: UpdateLineItemAdjustmentDTO - } - > {} - -export interface IShippingMethodAdjustmentRepository< - TEntity extends ShippingMethodAdjustment = ShippingMethodAdjustment -> extends DAL.RepositoryService< - TEntity, - { - create: CreateShippingMethodAdjustmentDTO - update: UpdateShippingMethodAdjustmentDTO - } - > {} - -export interface IShippingMethodTaxLineRepository< - TEntity extends ShippingMethodTaxLine = ShippingMethodTaxLine -> extends DAL.RepositoryService< - TEntity, - { - create: CreateShippingMethodTaxLineDTO - update: UpdateShippingMethodTaxLineDTO - } - > {} - -export interface ILineItemTaxLineRepository< - TEntity extends LineItemTaxLine = LineItemTaxLine -> extends DAL.RepositoryService< - TEntity, - { - create: CreateLineItemTaxLineDTO - update: UpdateLineItemTaxLineDTO - } - > {} diff --git a/packages/cart/src/types/shipping-method.ts b/packages/cart/src/types/shipping-method.ts index 6e70a4056bc0c..6d15473f1b6ea 100644 --- a/packages/cart/src/types/shipping-method.ts +++ b/packages/cart/src/types/shipping-method.ts @@ -1,6 +1,6 @@ export interface CreateShippingMethodDTO { name: string - cart_id: string + shippingMethod_id: string amount: number data?: Record } diff --git a/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts b/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts index b2b074842c701..d9ad730678c8a 100644 --- a/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts +++ b/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts @@ -12,7 +12,7 @@ export const deleteCustomerGroupStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.softDeleteCustomerGroup(ids) + await service.softDeleteCustomerGroups(ids) return new StepResponse(void 0, ids) }, @@ -25,6 +25,6 @@ export const deleteCustomerGroupStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.restoreCustomerGroup(prevCustomerGroups) + await service.restoreCustomerGroups(prevCustomerGroups) } ) diff --git a/packages/core-flows/src/customer-group/steps/update-customer-groups.ts b/packages/core-flows/src/customer-group/steps/update-customer-groups.ts index 553f1d5ae6c09..7acced28ebe40 100644 --- a/packages/core-flows/src/customer-group/steps/update-customer-groups.ts +++ b/packages/core-flows/src/customer-group/steps/update-customer-groups.ts @@ -1,8 +1,8 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { + CustomerGroupUpdatableFields, FilterableCustomerGroupProps, ICustomerModuleService, - CustomerGroupUpdatableFields, } from "@medusajs/types" import { getSelectsAndRelationsFromObjectArray, @@ -31,7 +31,7 @@ export const updateCustomerGroupsStep = createStep( relations, }) - const customers = await service.updateCustomerGroup( + const customers = await service.updateCustomerGroups( data.selector, data.update ) @@ -49,7 +49,7 @@ export const updateCustomerGroupsStep = createStep( await promiseAll( prevCustomerGroups.map((c) => - service.updateCustomerGroup(c.id, { + service.updateCustomerGroups(c.id, { name: c.name, }) ) diff --git a/packages/core-flows/src/customer/steps/create-addresses.ts b/packages/core-flows/src/customer/steps/create-addresses.ts index 139aebf0d248a..a14686069d371 100644 --- a/packages/core-flows/src/customer/steps/create-addresses.ts +++ b/packages/core-flows/src/customer/steps/create-addresses.ts @@ -1,8 +1,8 @@ import { - ICustomerModuleService, CreateCustomerAddressDTO, + ICustomerModuleService, } from "@medusajs/types" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk" export const createCustomerAddressesStepId = "create-customer-addresses" @@ -29,6 +29,6 @@ export const createCustomerAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.deleteAddress(ids) + await service.deleteAddresses(ids) } ) diff --git a/packages/core-flows/src/customer/steps/delete-addresses.ts b/packages/core-flows/src/customer/steps/delete-addresses.ts index c6ed1732993ac..46bfcab654cf7 100644 --- a/packages/core-flows/src/customer/steps/delete-addresses.ts +++ b/packages/core-flows/src/customer/steps/delete-addresses.ts @@ -14,7 +14,7 @@ export const deleteCustomerAddressesStep = createStep( const existing = await service.listAddresses({ id: ids, }) - await service.deleteAddress(ids) + await service.deleteAddresses(ids) return new StepResponse(void 0, existing) }, diff --git a/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts b/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts index 50a2b36aca19d..70212f2c51482 100644 --- a/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts +++ b/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts @@ -1,12 +1,12 @@ import { - ICustomerModuleService, CreateCustomerAddressDTO, - FilterableCustomerAddressProps, CustomerAddressDTO, + FilterableCustomerAddressProps, + ICustomerModuleService, } from "@medusajs/types" import { createStep } from "@medusajs/workflows-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { unsetForUpdate, unsetForCreate } from "./utils" +import { unsetForCreate, unsetForUpdate } from "./utils" import { isDefined } from "@medusajs/utils" type StepInput = { @@ -53,7 +53,7 @@ export const maybeUnsetDefaultBillingAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - await customerModuleService.updateAddress( + await customerModuleService.updateAddresses( { id: addressesToSet }, { is_default_billing: true } ) diff --git a/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts b/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts index 7ffbaf41b0e16..b484c31a5f089 100644 --- a/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts +++ b/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts @@ -1,12 +1,12 @@ import { - ICustomerModuleService, CreateCustomerAddressDTO, - FilterableCustomerAddressProps, CustomerAddressDTO, + FilterableCustomerAddressProps, + ICustomerModuleService, } from "@medusajs/types" import { createStep } from "@medusajs/workflows-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { unsetForUpdate, unsetForCreate } from "./utils" +import { unsetForCreate, unsetForUpdate } from "./utils" import { isDefined } from "@medusajs/utils" type StepInput = { @@ -52,7 +52,7 @@ export const maybeUnsetDefaultShippingAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - await customerModuleService.updateAddress( + await customerModuleService.updateAddresses( { id: addressesToSet }, { is_default_shipping: true } ) diff --git a/packages/core-flows/src/customer/steps/update-addresses.ts b/packages/core-flows/src/customer/steps/update-addresses.ts index 17d7d68be1901..49b793e36b09b 100644 --- a/packages/core-flows/src/customer/steps/update-addresses.ts +++ b/packages/core-flows/src/customer/steps/update-addresses.ts @@ -31,7 +31,7 @@ export const updateCustomerAddressesStep = createStep( relations, }) - const customerAddresses = await service.updateAddress( + const customerAddresses = await service.updateAddresses( data.selector, data.update ) @@ -48,7 +48,7 @@ export const updateCustomerAddressesStep = createStep( ) await promiseAll( - prevCustomerAddresses.map((c) => service.updateAddress(c.id, { ...c })) + prevCustomerAddresses.map((c) => service.updateAddresses(c.id, { ...c })) ) } ) diff --git a/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts b/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts index 25c6e15df2479..870e67a7a416b 100644 --- a/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts +++ b/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts @@ -21,7 +21,7 @@ export const unsetForCreate = async ( [field]: true, }) - await customerService.updateAddress( + await customerService.updateAddresses( { customer_id: customerIds, [field]: true }, { [field]: false } ) diff --git a/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts b/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts index aa12bb2122f2b..404b550a9cf5a 100644 --- a/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts +++ b/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts @@ -28,7 +28,7 @@ export const unsetForUpdate = async ( [field]: true, }) - await customerService.updateAddress( + await customerService.updateAddresses( { customer_id: customerIds, [field]: true }, { [field]: false } ) diff --git a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts index 26beb2c4abf66..ae5236c734d76 100644 --- a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts +++ b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts @@ -535,7 +535,7 @@ describe("Customer Module Service", () => { await service.delete(customer.id) - const res = await service.listCustomerGroupRelations({ + const res = await service.listCustomerGroupCustomers({ customer_id: customer.id, customer_group_id: group.id, }) @@ -546,7 +546,7 @@ describe("Customer Module Service", () => { describe("deleteCustomerGroup", () => { it("should delete a single customer group", async () => { const [group] = await service.createCustomerGroup([{ name: "VIP" }]) - await service.deleteCustomerGroup(group.id) + await service.deleteCustomerGroups(group.id) await expect( service.retrieveCustomerGroup(group.id) @@ -560,7 +560,7 @@ describe("Customer Module Service", () => { ]) const groupIds = groups.map((group) => group.id) - await service.deleteCustomerGroup(groupIds) + await service.deleteCustomerGroups(groupIds) for (const group of groups) { await expect( @@ -575,7 +575,7 @@ describe("Customer Module Service", () => { await service.createCustomerGroup([{ name: "VIP" }, { name: "Regular" }]) const selector = { name: "VIP" } - await service.deleteCustomerGroup(selector) + await service.deleteCustomerGroups(selector) const remainingGroups = await service.listCustomerGroups({ name: "VIP" }) expect(remainingGroups.length).toBe(0) @@ -595,9 +595,9 @@ describe("Customer Module Service", () => { customer_group_id: group.id, }) - await service.deleteCustomerGroup(group.id) + await service.deleteCustomerGroups(group.id) - const res = await service.listCustomerGroupRelations({ + const res = await service.listCustomerGroupCustomers({ customer_id: customer.id, customer_group_id: group.id, }) @@ -743,7 +743,7 @@ describe("Customer Module Service", () => { address_1: "123 Main St", }) - await service.updateAddress(address.id, { + await service.updateAddresses(address.id, { address_name: "Work", address_1: "456 Main St", }) @@ -778,7 +778,7 @@ describe("Customer Module Service", () => { address_1: "456 Main St", }) - await service.updateAddress( + await service.updateAddresses( { customer_id: customer.id }, { address_name: "Under Construction", @@ -822,7 +822,7 @@ describe("Customer Module Service", () => { }, ]) - await service.updateAddress([address1.id, address2.id], { + await service.updateAddresses([address1.id, address2.id], { address_name: "Under Construction", }) @@ -864,7 +864,7 @@ describe("Customer Module Service", () => { }) await expect( - service.updateAddress(address.id, { is_default_shipping: true }) + service.updateAddresses(address.id, { is_default_shipping: true }) ).rejects.toThrow("A default shipping address already exists") }) }) @@ -1087,7 +1087,7 @@ describe("Customer Module Service", () => { describe("softDeleteCustomerGroup", () => { it("should soft delete a single customer group", async () => { const [group] = await service.createCustomerGroup([{ name: "VIP" }]) - await service.softDeleteCustomerGroup([group.id]) + await service.softDeleteCustomerGroups([group.id]) const res = await service.listCustomerGroups({ id: group.id }) expect(res.length).toBe(0) @@ -1105,7 +1105,7 @@ describe("Customer Module Service", () => { { name: "Regular" }, ]) const groupIds = groups.map((group) => group.id) - await service.softDeleteCustomerGroup(groupIds) + await service.softDeleteCustomerGroups(groupIds) const res = await service.listCustomerGroups({ id: groupIds }) expect(res.length).toBe(0) @@ -1121,12 +1121,12 @@ describe("Customer Module Service", () => { describe("restoreCustomerGroup", () => { it("should restore a single customer group", async () => { const [group] = await service.createCustomerGroup([{ name: "VIP" }]) - await service.softDeleteCustomerGroup([group.id]) + await service.softDeleteCustomerGroups([group.id]) const res = await service.listCustomerGroups({ id: group.id }) expect(res.length).toBe(0) - await service.restoreCustomerGroup([group.id]) + await service.restoreCustomerGroups([group.id]) const restoredGroup = await service.retrieveCustomerGroup(group.id, { withDeleted: true, @@ -1140,12 +1140,12 @@ describe("Customer Module Service", () => { { name: "Regular" }, ]) const groupIds = groups.map((group) => group.id) - await service.softDeleteCustomerGroup(groupIds) + await service.softDeleteCustomerGroups(groupIds) const res = await service.listCustomerGroups({ id: groupIds }) expect(res.length).toBe(0) - await service.restoreCustomerGroup(groupIds) + await service.restoreCustomerGroups(groupIds) const restoredGroups = await service.listCustomerGroups( { id: groupIds }, diff --git a/packages/customer/src/models/address.ts b/packages/customer/src/models/address.ts index 13aacd7c005a5..817d4c2771cc4 100644 --- a/packages/customer/src/models/address.ts +++ b/packages/customer/src/models/address.ts @@ -2,27 +2,32 @@ import { DAL } from "@medusajs/types" import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Entity, + Index, + ManyToOne, OnInit, OptionalProps, PrimaryKey, Property, - ManyToOne, - Cascade, - Index, } from "@mikro-orm/core" import Customer from "./customer" type OptionalAddressProps = DAL.EntityDateColumns // TODO: To be revisited when more clear +export const UNIQUE_CUSTOMER_SHIPPING_ADDRESS = + "IDX_customer_address_unique_customer_shipping" +export const UNIQUE_CUSTOMER_BILLING_ADDRESS = + "IDX_customer_address_unique_customer_billing" + @Entity({ tableName: "customer_address" }) @Index({ - name: "IDX_customer_address_unique_customer_shipping", + name: UNIQUE_CUSTOMER_SHIPPING_ADDRESS, expression: 'create unique index "IDX_customer_address_unique_customer_shipping" on "customer_address" ("customer_id") where "is_default_shipping" = true', }) @Index({ - name: "IDX_customer_address_unique_customer_billing", + name: UNIQUE_CUSTOMER_BILLING_ADDRESS, expression: 'create unique index "IDX_customer_address_unique_customer_billing" on "customer_address" ("customer_id") where "is_default_billing" = true', }) diff --git a/packages/customer/src/services/address.ts b/packages/customer/src/services/address.ts deleted file mode 100644 index 383a07707bc37..0000000000000 --- a/packages/customer/src/services/address.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Address } from "@models" -import { CreateAddressDTO, UpdateAddressDTO } from "@types" - -type InjectedDependencies = { - addressRepository: DAL.RepositoryService -} - -export default class AddressService< - TEntity extends Address = Address -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateAddressDTO - update: UpdateAddressDTO - } ->(Address) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/customer-group-customer.ts b/packages/customer/src/services/customer-group-customer.ts deleted file mode 100644 index cc61f576af834..0000000000000 --- a/packages/customer/src/services/customer-group-customer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CustomerGroupCustomer } from "@models" - -type CreateCustomerGroupCustomerDTO = { - customer_id: string - customer_group_id: string - created_by?: string -} - -type InjectedDependencies = { - customerGroupRepository: DAL.RepositoryService -} - -export default class CustomerGroupCustomerService< - TEntity extends CustomerGroupCustomer = CustomerGroupCustomer -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { create: CreateCustomerGroupCustomerDTO } ->(CustomerGroupCustomer) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/customer-group.ts b/packages/customer/src/services/customer-group.ts deleted file mode 100644 index 830e949c58ddb..0000000000000 --- a/packages/customer/src/services/customer-group.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CustomerGroup } from "@models" -import { CreateCustomerGroupDTO, UpdateCustomerGroupDTO } from "@medusajs/types" - -type InjectedDependencies = { - customerGroupRepository: DAL.RepositoryService -} - -export default class CustomerGroupService< - TEntity extends CustomerGroup = CustomerGroup -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCustomerGroupDTO - update: UpdateCustomerGroupDTO - } ->(CustomerGroup) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index 7a41d481b84f9..49b6422e80a94 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -1,48 +1,69 @@ import { Context, + CustomerDTO, + CustomerTypes, DAL, - FindConfig, ICustomerModuleService, InternalModuleDeclaration, ModuleJoinerConfig, - CustomerTypes, - SoftDeleteReturn, - RestoreReturn, + ModulesSdkTypes, } from "@medusajs/types" import { InjectManager, InjectTransactionManager, - MedusaContext, - mapObjectTo, - isString, - isObject, isDuplicateError, + isString, + MedusaContext, + MedusaError, + ModulesSdkUtils, } from "@medusajs/utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" -import * as services from "../services" -import { MedusaError } from "@medusajs/utils" +import { + Address, + Customer, + CustomerGroup, + CustomerGroupCustomer, +} from "@models" import { EntityManager } from "@mikro-orm/core" - -const UNIQUE_CUSTOMER_SHIPPING_ADDRESS = - "IDX_customer_address_unique_customer_shipping" -const UNIQUE_CUSTOMER_BILLING_ADDRESS = - "IDX_customer_address_unique_customer_billing" +import { + UNIQUE_CUSTOMER_BILLING_ADDRESS, + UNIQUE_CUSTOMER_SHIPPING_ADDRESS, +} from "../models/address" type InjectedDependencies = { baseRepository: DAL.RepositoryService - customerService: services.CustomerService - addressService: services.AddressService - customerGroupService: services.CustomerGroupService - customerGroupCustomerService: services.CustomerGroupCustomerService + customerService: ModulesSdkTypes.InternalModuleService + addressService: ModulesSdkTypes.InternalModuleService + customerGroupService: ModulesSdkTypes.InternalModuleService + customerGroupCustomerService: ModulesSdkTypes.InternalModuleService } -export default class CustomerModuleService implements ICustomerModuleService { +const generateMethodForModels = [Address, CustomerGroup, CustomerGroupCustomer] + +export default class CustomerModuleService< + TAddress extends Address = Address, + TCustomer extends Customer = Customer, + TCustomerGroup extends CustomerGroup = CustomerGroup, + TCustomerGroupCustomer extends CustomerGroupCustomer = CustomerGroupCustomer + > + // TODO seb I let you manage that when you are moving forward + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + CustomerDTO, + { + Address: { dto: any } + CustomerGroup: { dto: any } + CustomerGroupCustomer: { dto: any } + } + >(Customer, generateMethodForModels, entityNameToLinkableKeysMap) + implements ICustomerModuleService +{ protected baseRepository_: DAL.RepositoryService - protected customerService_: services.CustomerService - protected addressService_: services.AddressService - protected customerGroupService_: services.CustomerGroupService - protected customerGroupCustomerService_: services.CustomerGroupCustomerService + protected customerService_: ModulesSdkTypes.InternalModuleService + protected addressService_: ModulesSdkTypes.InternalModuleService + protected customerGroupService_: ModulesSdkTypes.InternalModuleService + protected customerGroupCustomerService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -54,6 +75,9 @@ export default class CustomerModuleService implements ICustomerModuleService { }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.customerService_ = customerService this.addressService_ = addressService @@ -65,26 +89,6 @@ export default class CustomerModuleService implements ICustomerModuleService { return joinerConfig } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const customer = await this.customerService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - customer, - { - populate: true, - } - ) - } - async create( data: CustomerTypes.CreateCustomerDTO, sharedContext?: Context @@ -95,13 +99,33 @@ export default class CustomerModuleService implements ICustomerModuleService { sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async create( dataOrArray: | CustomerTypes.CreateCustomerDTO | CustomerTypes.CreateCustomerDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { + const customers = await this.create_(dataOrArray, sharedContext).catch( + this.handleDbErrors + ) + + const serialized = await this.baseRepository_.serialize< + CustomerTypes.CustomerDTO[] + >(customers, { + populate: true, + }) + + return Array.isArray(dataOrArray) ? serialized : serialized[0] + } + + @InjectTransactionManager("baseRepository_") + async create_( + dataOrArray: + | CustomerTypes.CreateCustomerDTO + | CustomerTypes.CreateCustomerDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] const customers = await this.customerService_.create(data, sharedContext) @@ -121,12 +145,7 @@ export default class CustomerModuleService implements ICustomerModuleService { await this.addAddresses(addressDataWithCustomerIds, sharedContext) - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerDTO[] - >(customers, { - populate: true, - }) - return Array.isArray(dataOrArray) ? serialized : serialized[0] + return customers as unknown as CustomerTypes.CustomerDTO[] } update( @@ -151,37 +170,38 @@ export default class CustomerModuleService implements ICustomerModuleService { data: CustomerTypes.CustomerUpdatableFields, @MedusaContext() sharedContext: Context = {} ) { - let updateData: CustomerTypes.UpdateCustomerDTO[] = [] + let updateData: + | CustomerTypes.UpdateCustomerDTO + | CustomerTypes.UpdateCustomerDTO[] + | { + selector: CustomerTypes.FilterableCustomerProps + data: CustomerTypes.CustomerUpdatableFields + } = [] + if (isString(idsOrSelector)) { - updateData = [ - { - id: idsOrSelector, - ...data, - }, - ] + updateData = { + id: idsOrSelector, + ...data, + } } else if (Array.isArray(idsOrSelector)) { updateData = idsOrSelector.map((id) => ({ id, ...data, })) } else { - const ids = await this.customerService_.list( - idsOrSelector, - { select: ["id"] }, - sharedContext - ) - updateData = ids.map(({ id }) => ({ - id, - ...data, - })) + updateData = { + selector: idsOrSelector, + data: data, + } } const customers = await this.customerService_.update( updateData, sharedContext ) + const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerDTO[] + CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[] >(customers, { populate: true, }) @@ -189,78 +209,6 @@ export default class CustomerModuleService implements ICustomerModuleService { return isString(idsOrSelector) ? serialized[0] : serialized } - delete(customerId: string, sharedContext?: Context): Promise - delete(customerIds: string[], sharedContext?: Context): Promise - delete( - selector: CustomerTypes.FilterableCustomerProps, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async delete( - idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps, - @MedusaContext() sharedContext: Context = {} - ) { - let toDelete = Array.isArray(idsOrSelector) - ? idsOrSelector - : [idsOrSelector as string] - if (isObject(idsOrSelector)) { - const ids = await this.customerService_.list( - idsOrSelector, - { - select: ["id"], - }, - sharedContext - ) - toDelete = ids.map(({ id }) => id) - } - - return await this.customerService_.delete(toDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async list( - filters: CustomerTypes.FilterableCustomerProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const customers = await this.customerService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - customers, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: CustomerTypes.FilterableCustomerProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CustomerTypes.CustomerDTO[], number]> { - const [customers, count] = await this.customerService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - customers, - { - populate: true, - } - ), - count, - ] - } - async createCustomerGroup( dataOrArrayOfData: CustomerTypes.CreateCustomerGroupDTO, sharedContext?: Context @@ -278,55 +226,36 @@ export default class CustomerModuleService implements ICustomerModuleService { | CustomerTypes.CreateCustomerGroupDTO[], @MedusaContext() sharedContext: Context = {} ) { - const data = Array.isArray(dataOrArrayOfData) - ? dataOrArrayOfData - : [dataOrArrayOfData] + const groups = await this.customerGroupService_.create( + dataOrArrayOfData, + sharedContext + ) - const groups = await this.customerGroupService_.create(data, sharedContext) - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupDTO[] + return await this.baseRepository_.serialize< + CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] >(groups, { populate: true, }) - - return Array.isArray(dataOrArrayOfData) ? serialized : serialized[0] } - @InjectManager("baseRepository_") - async retrieveCustomerGroup( - groupId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const group = await this.customerGroupService_.retrieve( - groupId, - config, - sharedContext - ) - return await this.baseRepository_.serialize( - group, - { populate: true } - ) - } - - async updateCustomerGroup( + async updateCustomerGroups( groupId: string, data: CustomerTypes.CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - async updateCustomerGroup( + async updateCustomerGroups( groupIds: string[], data: CustomerTypes.CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - async updateCustomerGroup( + async updateCustomerGroups( selector: CustomerTypes.FilterableCustomerGroupProps, data: CustomerTypes.CustomerGroupUpdatableFields, sharedContext?: Context ): Promise @InjectTransactionManager("baseRepository_") - async updateCustomerGroup( + async updateCustomerGroups( groupIdOrSelector: | string | string[] @@ -334,29 +263,27 @@ export default class CustomerModuleService implements ICustomerModuleService { data: CustomerTypes.CustomerGroupUpdatableFields, @MedusaContext() sharedContext: Context = {} ) { - let updateData: CustomerTypes.UpdateCustomerGroupDTO[] = [] - if (isString(groupIdOrSelector)) { - updateData = [ - { - id: groupIdOrSelector, - ...data, - }, - ] - } else if (Array.isArray(groupIdOrSelector)) { - updateData = groupIdOrSelector.map((id) => ({ + let updateData: + | CustomerTypes.UpdateCustomerGroupDTO + | CustomerTypes.UpdateCustomerGroupDTO[] + | { + selector: CustomerTypes.FilterableCustomerGroupProps + data: CustomerTypes.CustomerGroupUpdatableFields + } = [] + + if (isString(groupIdOrSelector) || Array.isArray(groupIdOrSelector)) { + const groupIdOrSelectorArray = Array.isArray(groupIdOrSelector) + ? groupIdOrSelector + : [groupIdOrSelector] + updateData = groupIdOrSelectorArray.map((id) => ({ id, ...data, })) } else { - const ids = await this.customerGroupService_.list( - groupIdOrSelector, - { select: ["id"] }, - sharedContext - ) - updateData = ids.map(({ id }) => ({ - id, - ...data, - })) + updateData = { + selector: groupIdOrSelector, + data: data, + } } const groups = await this.customerGroupService_.update( @@ -376,39 +303,6 @@ export default class CustomerModuleService implements ICustomerModuleService { >(groups, { populate: true }) } - deleteCustomerGroup(groupId: string, sharedContext?: Context): Promise - deleteCustomerGroup( - groupIds: string[], - sharedContext?: Context - ): Promise - deleteCustomerGroup( - selector: CustomerTypes.FilterableCustomerGroupProps, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async deleteCustomerGroup( - groupIdOrSelector: - | string - | string[] - | CustomerTypes.FilterableCustomerGroupProps, - @MedusaContext() sharedContext: Context = {} - ) { - let toDelete = Array.isArray(groupIdOrSelector) - ? groupIdOrSelector - : [groupIdOrSelector as string] - if (isObject(groupIdOrSelector)) { - const ids = await this.customerGroupService_.list( - groupIdOrSelector, - { select: ["id"] }, - sharedContext - ) - toDelete = ids.map(({ id }) => id) - } - - return await this.customerGroupService_.delete(toDelete, sharedContext) - } - async addCustomerToGroup( groupCustomerPair: CustomerTypes.GroupCustomerPair, sharedContext?: Context @@ -425,17 +319,20 @@ export default class CustomerModuleService implements ICustomerModuleService { @MedusaContext() sharedContext: Context = {} ): Promise<{ id: string } | { id: string }[]> { const groupCustomers = await this.customerGroupCustomerService_.create( - Array.isArray(data) ? data : [data], + data, sharedContext ) if (Array.isArray(data)) { - return groupCustomers.map((gc) => ({ id: gc.id })) + return (groupCustomers as unknown as TCustomerGroupCustomer[]).map( + (gc) => ({ id: gc.id }) + ) } - return { id: groupCustomers[0].id } + return { id: groupCustomers.id } } + // TODO: should be createAddresses to conform to the convention async addAddresses( addresses: CustomerTypes.CreateCustomerAddressDTO[], sharedContext?: Context @@ -445,7 +342,7 @@ export default class CustomerModuleService implements ICustomerModuleService { sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async addAddresses( data: | CustomerTypes.CreateCustomerAddressDTO @@ -454,13 +351,10 @@ export default class CustomerModuleService implements ICustomerModuleService { ): Promise< CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] > { - const addresses = await this.addressService_.create( - Array.isArray(data) ? data : [data], - sharedContext + const addresses = await this.addAddresses_(data, sharedContext).catch( + this.handleDbErrors ) - await this.flush(sharedContext).catch(this.handleDbErrors) - const serialized = await this.baseRepository_.serialize< CustomerTypes.CustomerAddressDTO[] >(addresses, { populate: true }) @@ -472,24 +366,39 @@ export default class CustomerModuleService implements ICustomerModuleService { return serialized[0] } - async updateAddress( + @InjectTransactionManager("baseRepository_") + private async addAddresses_( + data: + | CustomerTypes.CreateCustomerAddressDTO + | CustomerTypes.CreateCustomerAddressDTO[], + @MedusaContext() sharedContext: Context = {} + ) { + const addresses = await this.addressService_.create( + Array.isArray(data) ? data : [data], + sharedContext + ) + + return addresses + } + + async updateAddresses( addressId: string, data: CustomerTypes.UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - async updateAddress( + async updateAddresses( addressIds: string[], data: CustomerTypes.UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - async updateAddress( + async updateAddresses( selector: CustomerTypes.FilterableCustomerAddressProps, data: CustomerTypes.UpdateCustomerAddressDTO, sharedContext?: Context ): Promise @InjectTransactionManager("baseRepository_") - async updateAddress( + async updateAddresses( addressIdOrSelector: | string | string[] @@ -497,7 +406,12 @@ export default class CustomerModuleService implements ICustomerModuleService { data: CustomerTypes.UpdateCustomerAddressDTO, @MedusaContext() sharedContext: Context = {} ) { - let updateData: CustomerTypes.UpdateCustomerAddressDTO[] = [] + let updateData: + | CustomerTypes.UpdateCustomerAddressDTO[] + | { + selector: CustomerTypes.FilterableCustomerAddressProps + data: CustomerTypes.UpdateCustomerAddressDTO + } = [] if (isString(addressIdOrSelector)) { updateData = [ { @@ -511,15 +425,10 @@ export default class CustomerModuleService implements ICustomerModuleService { ...data, })) } else { - const ids = await this.addressService_.list( - addressIdOrSelector, - { select: ["id"] }, - sharedContext - ) - updateData = ids.map(({ id }) => ({ - id, - ...data, - })) + updateData = { + selector: addressIdOrSelector, + data, + } } const addresses = await this.addressService_.update( @@ -540,78 +449,6 @@ export default class CustomerModuleService implements ICustomerModuleService { return serialized } - async deleteAddress(addressId: string, sharedContext?: Context): Promise - async deleteAddress( - addressIds: string[], - sharedContext?: Context - ): Promise - async deleteAddress( - selector: CustomerTypes.FilterableCustomerAddressProps, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async deleteAddress( - addressIdOrSelector: - | string - | string[] - | CustomerTypes.FilterableCustomerAddressProps, - @MedusaContext() sharedContext: Context = {} - ) { - let toDelete = Array.isArray(addressIdOrSelector) - ? addressIdOrSelector - : [addressIdOrSelector as string] - - if (isObject(addressIdOrSelector)) { - const ids = await this.addressService_.list( - addressIdOrSelector, - { select: ["id"] }, - sharedContext - ) - toDelete = ids.map(({ id }) => id) - } - - await this.addressService_.delete(toDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async listAddresses( - filters?: CustomerTypes.FilterableCustomerAddressProps, - config?: FindConfig, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const addresses = await this.addressService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CustomerTypes.CustomerAddressDTO[] - >(addresses, { populate: true }) - } - - @InjectManager("baseRepository_") - async listAndCountAddresses( - filters?: CustomerTypes.FilterableCustomerAddressProps, - config?: FindConfig, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CustomerTypes.CustomerAddressDTO[], number]> { - const [addresses, count] = await this.addressService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - addresses, - { populate: true } - ), - count, - ] - } - async removeCustomerFromGroup( groupCustomerPair: CustomerTypes.GroupCustomerPair, sharedContext?: Context @@ -636,153 +473,6 @@ export default class CustomerModuleService implements ICustomerModuleService { ) } - @InjectManager("baseRepository_") - async listCustomerGroupRelations( - filters?: CustomerTypes.FilterableCustomerGroupCustomerProps, - config?: FindConfig, - @MedusaContext() sharedContext: Context = {} - ) { - const groupCustomers = await this.customerGroupCustomerService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupCustomerDTO[] - >(groupCustomers, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listCustomerGroups( - filters: CustomerTypes.FilterableCustomerGroupProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const groups = await this.customerGroupService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupDTO[] - >(groups, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCountCustomerGroups( - filters: CustomerTypes.FilterableCustomerGroupProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CustomerTypes.CustomerGroupDTO[], number]> { - const [groups, count] = await this.customerGroupService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - groups, - { - populate: true, - } - ), - count, - ] - } - - @InjectTransactionManager("baseRepository_") - async softDeleteCustomerGroup< - TReturnableLinkableKeys extends string = string - >( - groupIds: string[], - config: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = - await this.customerGroupService_.softDelete(groupIds, sharedContext) - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restoreCustomerGroup( - groupIds: string[], - config: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = await this.customerGroupService_.restore( - groupIds, - sharedContext - ) - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - - @InjectTransactionManager("baseRepository_") - async softDelete( - customerIds: string[], - config: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = await this.customerService_.softDelete( - customerIds, - sharedContext - ) - - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore( - customerIds: string[], - config: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = await this.customerService_.restore( - customerIds, - sharedContext - ) - - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - private async flush(context: Context) { const em = (context.manager ?? context.transactionManager) as EntityManager await em.flush() diff --git a/packages/customer/src/services/customer.ts b/packages/customer/src/services/customer.ts deleted file mode 100644 index 3aec4769e441d..0000000000000 --- a/packages/customer/src/services/customer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CustomerTypes, DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Customer } from "@models" - -type InjectedDependencies = { - customerRepository: DAL.RepositoryService -} - -export default class CustomerService< - TEntity extends Customer = Customer -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CustomerTypes.CreateCustomerDTO - update: CustomerTypes.UpdateCustomerDTO - } ->(Customer) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/index.ts b/packages/customer/src/services/index.ts index 07e39a5c1c62d..398679936abf0 100644 --- a/packages/customer/src/services/index.ts +++ b/packages/customer/src/services/index.ts @@ -1,5 +1 @@ -export { default as AddressService } from "./address" -export { default as CustomerGroupService } from "./customer-group" -export { default as CustomerService } from "./customer" export { default as CustomerModuleService } from "./customer-module" -export { default as CustomerGroupCustomerService } from "./customer-group-customer" diff --git a/packages/customer/src/types/index.ts b/packages/customer/src/types/index.ts index e70f89b103e9e..c993481e0d955 100644 --- a/packages/customer/src/types/index.ts +++ b/packages/customer/src/types/index.ts @@ -1,5 +1,8 @@ import { Logger } from "@medusajs/types" -export * from "./address" + +export * as ServiceTypes from "./services" +export * from "./services" + export type InitializeModuleInjectableDependencies = { logger?: Logger } diff --git a/packages/customer/src/types/address.ts b/packages/customer/src/types/services/address.ts similarity index 100% rename from packages/customer/src/types/address.ts rename to packages/customer/src/types/services/address.ts diff --git a/packages/customer/src/types/services/customer-group-customer.ts b/packages/customer/src/types/services/customer-group-customer.ts new file mode 100644 index 0000000000000..f51d3b57abbee --- /dev/null +++ b/packages/customer/src/types/services/customer-group-customer.ts @@ -0,0 +1,5 @@ +export interface CreateCustomerGroupCustomerDTO { + customer_id: string + customer_group_id: string + created_by?: string +} diff --git a/packages/customer/src/types/services/index.ts b/packages/customer/src/types/services/index.ts new file mode 100644 index 0000000000000..c7ac451a5e8be --- /dev/null +++ b/packages/customer/src/types/services/index.ts @@ -0,0 +1,2 @@ +export * from "./address" +export * from "./customer-group-customer" diff --git a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index 4aa4ba692695a..4f13d0822f622 100644 --- a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -116,7 +116,7 @@ describe("Payment Module Service", () => { expect(collection.length).toEqual(1) - await service.deletePaymentCollection(["pay-col-id-1"]) + await service.deletePaymentCollections(["pay-col-id-1"]) collection = await service.listPaymentCollections({ id: ["pay-col-id-1"], diff --git a/packages/payment/src/services/index.ts b/packages/payment/src/services/index.ts index 01f593ba08ead..a4f5ea37aea2e 100644 --- a/packages/payment/src/services/index.ts +++ b/packages/payment/src/services/index.ts @@ -1,2 +1 @@ export { default as PaymentModuleService } from "./payment-module" -export { default as PaymentCollectionService } from "./payment-collection" diff --git a/packages/payment/src/services/payment-collection.ts b/packages/payment/src/services/payment-collection.ts deleted file mode 100644 index abeef3e6625e7..0000000000000 --- a/packages/payment/src/services/payment-collection.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PaymentCollection } from "@models" -import { - CreatePaymentCollectionDTO, - DAL, - UpdatePaymentCollectionDTO, -} from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" - -type InjectedDependencies = { - paymentCollectionRepository: DAL.RepositoryService -} - -export default class PaymentCollectionService< - TEntity extends PaymentCollection = PaymentCollection -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePaymentCollectionDTO - update: UpdatePaymentCollectionDTO - } ->(PaymentCollection) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/payment/src/services/payment-module.ts b/packages/payment/src/services/payment-module.ts index 93d4a1310e4e1..1e62e664311eb 100644 --- a/packages/payment/src/services/payment-module.ts +++ b/packages/payment/src/services/payment-module.ts @@ -4,11 +4,10 @@ import { CreatePaymentDTO, CreatePaymentSessionDTO, DAL, - FilterablePaymentCollectionProps, - FindConfig, InternalModuleDeclaration, IPaymentModuleService, ModuleJoinerConfig, + ModulesSdkTypes, PaymentCollectionDTO, PaymentDTO, SetPaymentSessionsDTO, @@ -16,28 +15,64 @@ import { UpdatePaymentDTO, } from "@medusajs/types" import { - InjectManager, InjectTransactionManager, MedusaContext, + ModulesSdkUtils, } from "@medusajs/utils" -import * as services from "@services" - -import { joinerConfig } from "../joiner-config" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" +import { + Capture, + Payment, + PaymentCollection, + PaymentMethodToken, + PaymentProvider, + PaymentSession, + Refund, +} from "@models" type InjectedDependencies = { baseRepository: DAL.RepositoryService - paymentCollectionService: services.PaymentCollectionService + paymentCollectionService: ModulesSdkTypes.InternalModuleService } -export default class PaymentModuleService implements IPaymentModuleService { +const generateMethodForModels = [ + Capture, + PaymentCollection, + PaymentMethodToken, + PaymentProvider, + PaymentSession, + Refund, +] + +export default class PaymentModuleService< + TPaymentCollection extends PaymentCollection = PaymentCollection + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + // TODO revisit when moving forward frane + InjectedDependencies, + PaymentDTO, + { + Capture: { dto: any } + PaymentCollection: { dto: any } + PaymentMethodToken: { dto: any } + PaymentProvider: { dto: any } + PaymentSession: { dto: any } + Refund: { dto: any } + } + >(Payment, generateMethodForModels, entityNameToLinkableKeysMap) + implements IPaymentModuleService +{ protected baseRepository_: DAL.RepositoryService - protected paymentCollectionService_: services.PaymentCollectionService + protected paymentCollectionService_: ModulesSdkTypes.InternalModuleService constructor( { baseRepository, paymentCollectionService }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.paymentCollectionService_ = paymentCollectionService @@ -105,85 +140,6 @@ export default class PaymentModuleService implements IPaymentModuleService { ) } - deletePaymentCollection( - paymentCollectionId: string[], - sharedContext?: Context - ): Promise - deletePaymentCollection( - paymentCollectionId: string, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async deletePaymentCollection( - ids: string | string[], - @MedusaContext() sharedContext?: Context - ): Promise { - const paymentCollectionIds = Array.isArray(ids) ? ids : [ids] - await this.paymentCollectionService_.delete( - paymentCollectionIds, - sharedContext - ) - } - - @InjectManager("baseRepository_") - async retrievePaymentCollection( - paymentCollectionId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const paymentCollection = await this.paymentCollectionService_.retrieve( - paymentCollectionId, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - paymentCollection, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listPaymentCollections( - filters: FilterablePaymentCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext?: Context - ): Promise { - const paymentCollections = await this.paymentCollectionService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - paymentCollections, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPaymentCollections( - filters: FilterablePaymentCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext?: Context - ): Promise<[PaymentCollectionDTO[], number]> { - const [paymentCollections, count] = - await this.paymentCollectionService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - paymentCollections, - { populate: true } - ), - count, - ] - } - /** * TODO */ diff --git a/packages/payment/src/types/repositories.ts b/packages/payment/src/types/repositories.ts deleted file mode 100644 index cc84166e9a0d9..0000000000000 --- a/packages/payment/src/types/repositories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - DAL, - CreatePaymentCollectionDTO, - UpdatePaymentCollectionDTO, -} from "@medusajs/types" - -import { PaymentCollection } from "@models" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPaymentCollectionRepository< - TEntity extends PaymentCollection = PaymentCollection -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePaymentCollectionDTO - update: UpdatePaymentCollectionDTO - } - > {} diff --git a/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts index 33400946d48b4..cf054ed3aebe3 100644 --- a/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts @@ -183,7 +183,7 @@ describe("Currency Service", () => { error = e } - expect(error.message).toEqual('"currencyCode" must be defined') + expect(error.message).toEqual("currency - code must be defined") }) it("should return currency based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts index ae6a8cf2b6285..618857e6db566 100644 --- a/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts @@ -258,7 +258,7 @@ describe("MoneyAmount Service", () => { error = e } - expect(error.message).toEqual('"moneyAmountId" must be defined') + expect(error.message).toEqual("moneyAmount - id must be defined") }) it("should return moneyAmount based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts index f677a10fc04de..3e953563ead2d 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts @@ -162,7 +162,7 @@ describe("PriceListRule Service", () => { error = e } - expect(error.message).toEqual('"priceListRuleId" must be defined') + expect(error.message).toEqual("priceListRule - id must be defined") }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts index 79c2985044ffe..026bc3186bc1e 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts @@ -157,7 +157,7 @@ describe("PriceList Service", () => { error = e } - expect(error.message).toEqual('"priceListId" must be defined') + expect(error.message).toEqual("priceList - id must be defined") }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts index 81c0eb3dff967..e8402dba64cd0 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts @@ -219,7 +219,7 @@ describe("PriceRule Service", () => { error = e } - expect(error.message).toEqual('"priceRuleId" must be defined') + expect(error.message).toEqual("priceRule - id must be defined") }) it("should return priceRule based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts index 4c80f1a47c46f..8f5848a1d0edf 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts @@ -168,7 +168,7 @@ describe("PriceSetMoneyAmountRules Service", () => { } expect(error.message).toEqual( - '"priceSetMoneyAmountRulesId" must be defined' + "priceSetMoneyAmountRules - id must be defined" ) }) diff --git a/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts index 70ef27e8b4840..161bced693161 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts @@ -305,7 +305,7 @@ describe("PriceSet Service", () => { error = e } - expect(error.message).toEqual('"priceSetId" must be defined') + expect(error.message).toEqual("priceSet - id must be defined") }) it("should return priceSet based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts index 3c76e717b8bf3..ae2865a504880 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts @@ -180,7 +180,7 @@ describe("PricingModule Service - Currency", () => { error = e } - expect(error.message).toEqual('"currencyCode" must be defined') + expect(error.message).toEqual("currency - code must be defined") }) it("should return currency based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts index 76adb62a22a17..c1e47a73a9083 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts @@ -245,7 +245,7 @@ describe("PricingModule Service - MoneyAmount", () => { error = e } - expect(error.message).toEqual('"moneyAmountId" must be defined') + expect(error.message).toEqual("moneyAmount - id must be defined") }) it("should return moneyAmount based on config select param", async () => { @@ -320,7 +320,7 @@ describe("PricingModule Service - MoneyAmount", () => { }) }) - describe("restoreDeletedMoneyAmounts", () => { + describe("restoreMoneyAmounts", () => { const id = "money-amount-USD" it("should restore softDeleted priceSetMoneyAmount and PriceRule when restoring soft-deleting money amount", async () => { @@ -330,7 +330,7 @@ describe("PricingModule Service - MoneyAmount", () => { await createPriceRules(testManager) await createPriceSetMoneyAmountRules(testManager) await service.softDeleteMoneyAmounts([id]) - await service.restoreDeletedMoneyAmounts([id]) + await service.restoreMoneyAmounts([id]) const [moneyAmount] = await service.listMoneyAmounts( { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts index f659615812d0c..713f8fb696d36 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts @@ -171,7 +171,7 @@ describe("PriceListRule Service", () => { error = e } - expect(error.message).toEqual('"priceListRuleId" must be defined') + expect(error.message).toEqual("priceListRule - id must be defined") }) }) @@ -283,7 +283,7 @@ describe("PriceListRule Service", () => { expect(priceList.price_list_rules).toEqual( expect.arrayContaining([ expect.objectContaining({ - rule_type: "rule-type-3", + rule_type: { id: "rule-type-3" }, price_list_rule_values: [ expect.objectContaining({ value: "sc-1" }), ], @@ -323,7 +323,7 @@ describe("PriceListRule Service", () => { expect(priceList.price_list_rules).toEqual( expect.arrayContaining([ expect.objectContaining({ - rule_type: "rule-type-3", + rule_type: { id: "rule-type-3" }, price_list_rule_values: expect.arrayContaining([ expect.objectContaining({ value: "sc-1" }), expect.objectContaining({ value: "sc-2" }), @@ -351,7 +351,7 @@ describe("PriceListRule Service", () => { ) expect(priceList.price_list_rules).toEqual([ - expect.objectContaining({ rule_type: "rule-type-2" }), + expect.objectContaining({ rule_type: { id: "rule-type-2" } }), ]) }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts index 774ceec504608..eb41c50b9f0b3 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts @@ -179,7 +179,7 @@ describe("PriceList Service", () => { error = e } - expect(error.message).toEqual('"priceListId" must be defined') + expect(error.message).toEqual("priceList - id must be defined") }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts index d447055216850..7f3a619ec81a1 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts @@ -224,7 +224,7 @@ describe("PricingModule Service - PriceRule", () => { error = e } - expect(error.message).toEqual('"priceRuleId" must be defined') + expect(error.message).toEqual("priceRule - id must be defined") }) it("should return PriceRule based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts index 076a0d2c3f483..db74001808c6c 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts @@ -192,7 +192,7 @@ describe("PricingModule Service - PriceSetMoneyAmountRules", () => { } expect(error.message).toEqual( - '"priceSetMoneyAmountRulesId" must be defined' + "priceSetMoneyAmountRules - id must be defined" ) }) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts index 41ffa5c4ddffe..ac6ee27716514 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts @@ -248,7 +248,7 @@ describe("PricingModule Service - PriceSet", () => { error = e } - expect(error.message).toEqual('"priceSetId" must be defined') + expect(error.message).toEqual("priceSet - id must be defined") }) it("should return priceSet based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts index 2011087b97348..2c04056122961 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts @@ -170,7 +170,7 @@ describe("PricingModuleService ruleType", () => { error = e } - expect(error.message).toEqual('"ruleTypeId" must be defined') + expect(error.message).toEqual("ruleType - id must be defined") }) it("should return ruleType based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts index a2f038ee968b5..df7431aa38f90 100644 --- a/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts @@ -164,7 +164,7 @@ describe("RuleType Service", () => { error = e } - expect(error.message).toEqual('"ruleTypeId" must be defined') + expect(error.message).toEqual("ruleType - id must be defined") }) it("should return ruleType based on config select param", async () => { diff --git a/packages/pricing/src/services/__fixtures__/currency.ts b/packages/pricing/src/services/__fixtures__/currency.ts index 27315b54dfdc2..6ecdd890225e3 100644 --- a/packages/pricing/src/services/__fixtures__/currency.ts +++ b/packages/pricing/src/services/__fixtures__/currency.ts @@ -1,6 +1,5 @@ import { Currency } from "@models" -import { CurrencyService } from "@services" -import { asClass, asValue, createContainer } from "awilix" +import { asValue } from "awilix" ;(Currency as any).meta = { /** @@ -10,10 +9,7 @@ import { asClass, asValue, createContainer } from "awilix" } export const nonExistingCurrencyCode = "non-existing-code" -export const mockContainer = createContainer() - -mockContainer.register({ - transaction: asValue(async (task) => await task()), +export const currencyRepositoryMock = { currencyRepository: asValue({ find: jest.fn().mockImplementation(async ({ where: { code } }) => { if (code === nonExistingCurrencyCode) { @@ -25,5 +21,4 @@ mockContainer.register({ findAndCount: jest.fn().mockResolvedValue([[], 0]), getFreshManager: jest.fn().mockResolvedValue({}), }), - currencyService: asClass(CurrencyService), -}) +} diff --git a/packages/pricing/src/services/__tests__/currency.spec.ts b/packages/pricing/src/services/__tests__/currency.spec.ts index 2c130ec5baedd..46cc0f789c9d4 100644 --- a/packages/pricing/src/services/__tests__/currency.spec.ts +++ b/packages/pricing/src/services/__tests__/currency.spec.ts @@ -1,18 +1,31 @@ import { - mockContainer, + currencyRepositoryMock, nonExistingCurrencyCode, } from "../__fixtures__/currency" +import { createMedusaContainer } from "@medusajs/utils" +import { asValue } from "awilix" +import ContainerLoader from "../../loaders/container" +import { MedusaContainer } from "@medusajs/types" const code = "existing-currency" describe("Currency service", function () { - beforeEach(function () { + let container: MedusaContainer + + beforeEach(async function () { jest.clearAllMocks() + + container = createMedusaContainer() + container.register("manager", asValue({})) + + await ContainerLoader({ container }) + + container.register(currencyRepositoryMock) }) it("should retrieve a currency", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") await currencyService.retrieve(code) @@ -33,8 +46,8 @@ describe("Currency service", function () { }) it("should fail to retrieve a currency", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const err = await currencyService .retrieve(nonExistingCurrencyCode) @@ -62,8 +75,8 @@ describe("Currency service", function () { }) it("should list currencys", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = {} const config = { @@ -88,8 +101,8 @@ describe("Currency service", function () { }) it("should list currencys with filters", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = { tags: { @@ -126,8 +139,8 @@ describe("Currency service", function () { }) it("should list currencys with filters and relations", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = { tags: { @@ -163,9 +176,9 @@ describe("Currency service", function () { ) }) - it("should list and count the currencys with filters and relations", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + it("should list and count the currencies with filters and relations", async function () { + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = { tags: { diff --git a/packages/pricing/src/services/currency.ts b/packages/pricing/src/services/currency.ts deleted file mode 100644 index d7df5abda4613..0000000000000 --- a/packages/pricing/src/services/currency.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Currency } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - currencyRepository: DAL.RepositoryService -} - -export default class CurrencyService< - TEntity extends Currency = Currency -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateCurrencyDTO - update: ServiceTypes.UpdateCurrencyDTO - } ->(Currency) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/index.ts b/packages/pricing/src/services/index.ts index 7e67ff0a5543c..5d08291b40cd5 100644 --- a/packages/pricing/src/services/index.ts +++ b/packages/pricing/src/services/index.ts @@ -1,12 +1,6 @@ -export { default as CurrencyService } from "./currency" -export { default as MoneyAmountService } from "./money-amount" export { default as PriceListService } from "./price-list" export { default as PriceListRuleService } from "./price-list-rule" export { default as PriceListRuleValueService } from "./price-list-rule-value" export { default as PriceRuleService } from "./price-rule" -export { default as PriceSetService } from "./price-set" -export { default as PriceSetMoneyAmountService } from "./price-set-money-amount" -export { default as PriceSetMoneyAmountRulesService } from "./price-set-money-amount-rules" -export { default as PriceSetRuleTypeService } from "./price-set-rule-type" export { default as PricingModuleService } from "./pricing-module" export { default as RuleTypeService } from "./rule-type" diff --git a/packages/pricing/src/services/money-amount.ts b/packages/pricing/src/services/money-amount.ts deleted file mode 100644 index b73422969e25e..0000000000000 --- a/packages/pricing/src/services/money-amount.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { MoneyAmount } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - moneyAmountRepository: DAL.RepositoryService -} - -export default class MoneyAmountService< - TEntity extends MoneyAmount = MoneyAmount -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateMoneyAmountDTO - update: ServiceTypes.UpdateMoneyAmountDTO - }, - { - list: ServiceTypes.FilterableMoneyAmountProps - listAndCount: ServiceTypes.FilterableMoneyAmountProps - } ->(MoneyAmount) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-list-rule-value.ts b/packages/pricing/src/services/price-list-rule-value.ts index 10b5cc9ed37c1..88d4f0241439d 100644 --- a/packages/pricing/src/services/price-list-rule-value.ts +++ b/packages/pricing/src/services/price-list-rule-value.ts @@ -9,26 +9,32 @@ type InjectedDependencies = { export default class PriceListRuleValueService< TEntity extends PriceListRuleValue = PriceListRuleValue -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - update: ServiceTypes.UpdatePriceListRuleValueDTO - }, - { - list: ServiceTypes.FilterablePriceListRuleValueProps - listAndCount: ServiceTypes.FilterablePriceListRuleValueProps - } ->(PriceListRuleValue) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceListRuleValue +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceListRuleValueDTO[], + context: Context + ): Promise + + create( + data: ServiceTypes.CreatePriceListRuleValueDTO, + context: Context + ): Promise + + async create( + data: + | ServiceTypes.CreatePriceListRuleValueDTO + | ServiceTypes.CreatePriceListRuleValueDTO[], context: Context = {} - ): Promise { - const priceListRuleValues = data.map((priceRuleValueData) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceListRuleValues = data_.map((priceRuleValueData) => { const { price_list_rule_id: priceListRuleId, ...priceRuleValue } = priceRuleValueData diff --git a/packages/pricing/src/services/price-list-rule.ts b/packages/pricing/src/services/price-list-rule.ts index cb9ddb7487bd8..438b712b2fa7a 100644 --- a/packages/pricing/src/services/price-list-rule.ts +++ b/packages/pricing/src/services/price-list-rule.ts @@ -9,27 +9,31 @@ type InjectedDependencies = { export default class PriceListRuleService< TEntity extends PriceListRule = PriceListRule -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceListRuleDTO - update: ServiceTypes.UpdatePriceListRuleDTO - }, - { - list: ServiceTypes.FilterablePriceListRuleProps - listAndCount: ServiceTypes.FilterablePriceListRuleProps - } ->(PriceListRule) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceListRule +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceListRuleDTO[], + sharedContext?: Context + ): Promise + create( + data: ServiceTypes.CreatePriceListRuleDTO, + sharedContext?: Context + ): Promise + + async create( + data: + | ServiceTypes.CreatePriceListRuleDTO + | ServiceTypes.CreatePriceListRuleDTO[], context: Context = {} - ): Promise { - const priceListRule = data.map((priceListRule) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceListRule = data_.map((priceListRule) => { const { price_list_id: priceListId, rule_type_id: ruleTypeId, @@ -50,11 +54,28 @@ export default class PriceListRuleService< return await super.create(priceListRule, context) } - async update( + // @ts-ignore + update( data: ServiceTypes.UpdatePriceListRuleDTO[], + context: Context + ): Promise + + // @ts-ignore + update( + data: ServiceTypes.UpdatePriceListRuleDTO, + context: Context + ): Promise + + // TODO add support for selector? and then rm ts ignore + // @ts-ignore + async update( + data: + | ServiceTypes.UpdatePriceListRuleDTO + | ServiceTypes.UpdatePriceListRuleDTO[], context: Context = {} - ): Promise { - const priceListRules = data.map((priceListRule) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceListRules = data_.map((priceListRule) => { const { price_list_id, rule_type_id, ...priceListRuleData } = priceListRule diff --git a/packages/pricing/src/services/price-list.ts b/packages/pricing/src/services/price-list.ts index c5ddc99398b30..9810a6a8da5c1 100644 --- a/packages/pricing/src/services/price-list.ts +++ b/packages/pricing/src/services/price-list.ts @@ -9,32 +9,45 @@ type InjectedDependencies = { export default class PriceListService< TEntity extends PriceList = PriceList -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - {}, - { - list: ServiceTypes.FilterablePriceListProps - listAndCount: ServiceTypes.FilterablePriceListProps - } ->(PriceList) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceList +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceListDTO[], sharedContext?: Context - ): Promise { - const priceLists = this.normalizePriceListDate(data) + ): Promise + create( + data: ServiceTypes.CreatePriceListDTO, + sharedContext?: Context + ): Promise + + async create( + data: ServiceTypes.CreatePriceListDTO | ServiceTypes.CreatePriceListDTO[], + sharedContext?: Context + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceLists = this.normalizePriceListDate(data_) return await super.create(priceLists, sharedContext) } + // @ts-ignore + update(data: any[], sharedContext?: Context): Promise + // @ts-ignore + update(data: any, sharedContext?: Context): Promise + + // TODO: Add support for selector? and then rm ts ignore + // @ts-ignore async update( - data: ServiceTypes.UpdatePriceListDTO[], + data: ServiceTypes.UpdatePriceListDTO | ServiceTypes.UpdatePriceListDTO[], sharedContext?: Context - ): Promise { - const priceLists = this.normalizePriceListDate(data) + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceLists = this.normalizePriceListDate(data_) return await super.update(priceLists, sharedContext) } diff --git a/packages/pricing/src/services/price-rule.ts b/packages/pricing/src/services/price-rule.ts index 1c3f365e5c8d7..a0d8ce13ca48f 100644 --- a/packages/pricing/src/services/price-rule.ts +++ b/packages/pricing/src/services/price-rule.ts @@ -10,26 +10,29 @@ type InjectedDependencies = { export default class PriceRuleService< TEntity extends PriceRule = PriceRule -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - update: ServiceTypes.UpdatePriceRuleDTO - }, - { - list: ServiceTypes.FilterablePriceRuleProps - listAndCount: ServiceTypes.FilterablePriceRuleProps - } ->(PriceRule) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceRule +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceRuleDTO[], sharedContext?: Context - ): Promise { - const toCreate = data.map((ruleData) => { + ): Promise + create( + data: ServiceTypes.CreatePriceRuleDTO, + sharedContext?: Context + ): Promise + + async create( + data: ServiceTypes.CreatePriceRuleDTO | ServiceTypes.CreatePriceRuleDTO[], + sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const toCreate = data_.map((ruleData) => { const ruleDataClone = { ...ruleData } as any ruleDataClone.rule_type ??= ruleData.rule_type_id diff --git a/packages/pricing/src/services/price-set-money-amount-rules.ts b/packages/pricing/src/services/price-set-money-amount-rules.ts deleted file mode 100644 index a1915770cf8f4..0000000000000 --- a/packages/pricing/src/services/price-set-money-amount-rules.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSetMoneyAmountRules } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetMoneyAmountRulesRepository: DAL.RepositoryService -} - -export default class PriceSetMoneyAmountRulesService< - TEntity extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceSetMoneyAmountRulesDTO - update: ServiceTypes.UpdatePriceSetMoneyAmountRulesDTO - }, - { - list: ServiceTypes.FilterablePriceSetMoneyAmountRulesProps - listAndCount: ServiceTypes.FilterablePriceSetMoneyAmountRulesProps - } ->(PriceSetMoneyAmountRules) { - constructor({ priceSetMoneyAmountRulesRepository }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-set-money-amount.ts b/packages/pricing/src/services/price-set-money-amount.ts deleted file mode 100644 index 78644213d605d..0000000000000 --- a/packages/pricing/src/services/price-set-money-amount.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSetMoneyAmount } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetMoneyAmountRepository: DAL.RepositoryService -} - -export default class PriceSetMoneyAmountService< - TEntity extends PriceSetMoneyAmount = PriceSetMoneyAmount -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceSetMoneyAmountDTO - update: ServiceTypes.UpdatePriceSetMoneyAmountDTO - }, - { - list: ServiceTypes.FilterablePriceSetMoneyAmountProps - listAndCount: ServiceTypes.FilterablePriceSetMoneyAmountProps - } ->(PriceSetMoneyAmount) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-set-rule-type.ts b/packages/pricing/src/services/price-set-rule-type.ts deleted file mode 100644 index 452050ebd6939..0000000000000 --- a/packages/pricing/src/services/price-set-rule-type.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSetRuleType } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetRuleTypeRepository: DAL.RepositoryService -} - -export default class PriceSetRuleTypeService< - TEntity extends PriceSetRuleType = PriceSetRuleType -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceSetRuleTypeDTO - update: ServiceTypes.UpdatePriceSetRuleTypeDTO - }, - { - list: ServiceTypes.FilterablePriceSetRuleTypeProps - listAndCount: ServiceTypes.FilterablePriceSetRuleTypeProps - } ->(PriceSetRuleType) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-set.ts b/packages/pricing/src/services/price-set.ts deleted file mode 100644 index c850f045d77cd..0000000000000 --- a/packages/pricing/src/services/price-set.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSet } from "@models" - -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetRepository: DAL.RepositoryService -} - -export default class PriceSetService< - TEntity extends PriceSet = PriceSet -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: Omit - update: Omit - }, - { - list: ServiceTypes.FilterablePriceSetProps - listAndCount: ServiceTypes.FilterablePriceSetProps - } ->(PriceSet) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index c96c6e639e871..b588915df32ed 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -4,27 +4,26 @@ import { CreateMoneyAmountDTO, CreatePriceListRuleDTO, DAL, - FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, PriceSetDTO, PricingContext, PricingFilters, PricingRepositoryService, PricingTypes, - RestoreReturn, RuleTypeDTO, } from "@medusajs/types" import { + arrayDifference, + deduplicate, + groupBy, InjectManager, InjectTransactionManager, MedusaContext, MedusaError, + ModulesSdkUtils, PriceListType, - arrayDifference, - deduplicate, - groupBy, - mapObjectTo, removeNullish, } from "@medusajs/utils" @@ -43,66 +42,86 @@ import { } from "@models" import { - CurrencyService, - MoneyAmountService, PriceListRuleService, PriceListRuleValueService, PriceListService, PriceRuleService, - PriceSetMoneyAmountRulesService, - PriceSetMoneyAmountService, - PriceSetRuleTypeService, - PriceSetService, RuleTypeService, } from "@services" -import { ServiceTypes } from "@types" -import { validatePriceListDates } from "@utils" -import { CreatePriceListRuleValueDTO } from "src/types/services" -import { - LinkableKeys, - entityNameToLinkableKeysMap, - joinerConfig, -} from "../joiner-config" +import {entityNameToLinkableKeysMap, joinerConfig} from "../joiner-config" +import {validatePriceListDates} from "@utils" +import {ServiceTypes} from "@types" + type InjectedDependencies = { baseRepository: DAL.RepositoryService pricingRepository: PricingRepositoryService - currencyService: CurrencyService - moneyAmountService: MoneyAmountService - priceSetService: PriceSetService - priceSetMoneyAmountRulesService: PriceSetMoneyAmountRulesService + currencyService: ModulesSdkTypes.InternalModuleService + moneyAmountService: ModulesSdkTypes.InternalModuleService + priceSetService: ModulesSdkTypes.InternalModuleService + priceSetMoneyAmountRulesService: ModulesSdkTypes.InternalModuleService ruleTypeService: RuleTypeService priceRuleService: PriceRuleService - priceSetRuleTypeService: PriceSetRuleTypeService - priceSetMoneyAmountService: PriceSetMoneyAmountService + priceSetRuleTypeService: ModulesSdkTypes.InternalModuleService + priceSetMoneyAmountService: ModulesSdkTypes.InternalModuleService priceListService: PriceListService priceListRuleService: PriceListRuleService priceListRuleValueService: PriceListRuleValueService } +const generateMethodForModels = [ + Currency, + MoneyAmount, + PriceList, + PriceListRule, + PriceListRuleValue, + PriceRule, + PriceSetMoneyAmount, + PriceSetMoneyAmountRules, + PriceSetRuleType, + RuleType, +] + export default class PricingModuleService< - TPriceSet extends PriceSet = PriceSet, - TMoneyAmount extends MoneyAmount = MoneyAmount, - TCurrency extends Currency = Currency, - TRuleType extends RuleType = RuleType, - TPriceSetMoneyAmountRules extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules, - TPriceRule extends PriceRule = PriceRule, - TPriceSetRuleType extends PriceSetRuleType = PriceSetRuleType, - TPriceSetMoneyAmount extends PriceSetMoneyAmount = PriceSetMoneyAmount, - TPriceList extends PriceList = PriceList, - TPriceListRule extends PriceListRule = PriceListRule, - TPriceListRuleValue extends PriceListRuleValue = PriceListRuleValue -> implements PricingTypes.IPricingModuleService + TPriceSet extends PriceSet = PriceSet, + TMoneyAmount extends MoneyAmount = MoneyAmount, + TCurrency extends Currency = Currency, + TRuleType extends RuleType = RuleType, + TPriceSetMoneyAmountRules extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules, + TPriceRule extends PriceRule = PriceRule, + TPriceSetRuleType extends PriceSetRuleType = PriceSetRuleType, + TPriceSetMoneyAmount extends PriceSetMoneyAmount = PriceSetMoneyAmount, + TPriceList extends PriceList = PriceList, + TPriceListRule extends PriceListRule = PriceListRule, + TPriceListRuleValue extends PriceListRuleValue = PriceListRuleValue + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + PricingTypes.PriceSetDTO, + { + Currency: { dto: PricingTypes.CurrencyDTO } + MoneyAmount: { dto: PricingTypes.MoneyAmountDTO } + PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO } + PriceSetMoneyAmountRules: { + dto: PricingTypes.PriceSetMoneyAmountRulesDTO + } + PriceRule: { dto: PricingTypes.PriceRuleDTO } + RuleType: { dto: PricingTypes.RuleTypeDTO } + PriceList: { dto: PricingTypes.PriceListDTO } + PriceListRule: { dto: PricingTypes.PriceListRuleDTO } + } + >(PriceSet, generateMethodForModels, entityNameToLinkableKeysMap) + implements PricingTypes.IPricingModuleService { protected baseRepository_: DAL.RepositoryService protected readonly pricingRepository_: PricingRepositoryService - protected readonly currencyService_: CurrencyService - protected readonly moneyAmountService_: MoneyAmountService + protected readonly currencyService_: ModulesSdkTypes.InternalModuleService + protected readonly moneyAmountService_: ModulesSdkTypes.InternalModuleService protected readonly ruleTypeService_: RuleTypeService - protected readonly priceSetService_: PriceSetService - protected readonly priceSetMoneyAmountRulesService_: PriceSetMoneyAmountRulesService + protected readonly priceSetService_: ModulesSdkTypes.InternalModuleService + protected readonly priceSetMoneyAmountRulesService_: ModulesSdkTypes.InternalModuleService protected readonly priceRuleService_: PriceRuleService - protected readonly priceSetRuleTypeService_: PriceSetRuleTypeService - protected readonly priceSetMoneyAmountService_: PriceSetMoneyAmountService + protected readonly priceSetRuleTypeService_: ModulesSdkTypes.InternalModuleService + protected readonly priceSetMoneyAmountService_: ModulesSdkTypes.InternalModuleService protected readonly priceListService_: PriceListService protected readonly priceListRuleService_: PriceListRuleService protected readonly priceListRuleValueService_: PriceListRuleValueService @@ -125,6 +144,9 @@ export default class PricingModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.pricingRepository_ = pricingRepository this.currencyService_ = currencyService @@ -215,66 +237,6 @@ export default class PricingModuleService< return JSON.parse(JSON.stringify(calculatedPrices)) } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceSet = await this.priceSetService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize(priceSet, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async list( - filters: PricingTypes.FilterablePriceSetProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceSets = await this.priceSetService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceSets, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: PricingTypes.FilterablePriceSetProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceSetDTO[], number]> { - const [priceSets, count] = await this.priceSetService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceSets, - { - populate: true, - } - ), - count, - ] - } - async create( data: PricingTypes.CreatePriceSetDTO, sharedContext?: Context @@ -743,7 +705,7 @@ export default class PricingModuleService< ) { const priceSets = await this.priceSetService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceSets, { populate: true, @@ -751,77 +713,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async delete( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceSetService_.delete(ids, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveMoneyAmount( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const moneyAmount = await this.moneyAmountService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - moneyAmount, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listMoneyAmounts( - filters: PricingTypes.FilterableMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const moneyAmounts = await this.moneyAmountService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - moneyAmounts, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountMoneyAmounts( - filters: PricingTypes.FilterableMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.MoneyAmountDTO[], number]> { - const [moneyAmounts, count] = await this.moneyAmountService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - moneyAmounts, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createMoneyAmounts( data: PricingTypes.CreateMoneyAmountDTO[], @@ -832,7 +723,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( moneyAmounts, { populate: true, @@ -850,7 +741,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( moneyAmounts, { populate: true, @@ -858,109 +749,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deleteMoneyAmounts( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.moneyAmountService_.delete(ids, sharedContext) - } - - @InjectTransactionManager("baseRepository_") - async softDeleteMoneyAmounts( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.moneyAmountService_.softDelete(ids, sharedContext) - } - - @InjectTransactionManager("baseRepository_") - async restoreDeletedMoneyAmounts< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.moneyAmountService_.restore( - ids, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectManager("baseRepository_") - async retrieveCurrency( - code: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const currency = await this.currencyService_.retrieve( - code, - config, - sharedContext - ) - - return this.baseRepository_.serialize(currency, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listCurrencies( - filters: PricingTypes.FilterableCurrencyProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const currencies = await this.currencyService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - currencies, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountCurrencies( - filters: PricingTypes.FilterableCurrencyProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.CurrencyDTO[], number]> { - const [currencies, count] = await this.currencyService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - currencies, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createCurrencies( data: PricingTypes.CreateCurrencyDTO[], @@ -968,7 +756,7 @@ export default class PricingModuleService< ) { const currencies = await this.currencyService_.create(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( currencies, { populate: true, @@ -983,7 +771,7 @@ export default class PricingModuleService< ) { const currencies = await this.currencyService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( currencies, { populate: true, @@ -991,74 +779,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deleteCurrencies( - currencyCodes: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.currencyService_.delete(currencyCodes, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveRuleType( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const ruleType = await this.ruleTypeService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize(ruleType, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listRuleTypes( - filters: PricingTypes.FilterableRuleTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const ruleTypes = await this.ruleTypeService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - ruleTypes, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountRuleTypes( - filters: PricingTypes.FilterableRuleTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.RuleTypeDTO[], number]> { - const [ruleTypes, count] = await this.ruleTypeService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - ruleTypes, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createRuleTypes( data: PricingTypes.CreateRuleTypeDTO[], @@ -1066,7 +786,7 @@ export default class PricingModuleService< ): Promise { const ruleTypes = await this.ruleTypeService_.create(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( ruleTypes, { populate: true, @@ -1081,7 +801,7 @@ export default class PricingModuleService< ): Promise { const ruleTypes = await this.ruleTypeService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( ruleTypes, { populate: true, @@ -1089,118 +809,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deleteRuleTypes( - ruleTypeIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.ruleTypeService_.delete(ruleTypeIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceSetMoneyAmountRules( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const record = await this.priceSetMoneyAmountRulesService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - record, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceSetMoneyAmountRules( - filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const records = await this.priceSetMoneyAmountRulesService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountRulesDTO[] - >(records, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCountPriceSetMoneyAmountRules( - filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceSetMoneyAmountRulesDTO[], number]> { - const [records, count] = - await this.priceSetMoneyAmountRulesService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountRulesDTO[] - >(records, { - populate: true, - }), - count, - ] - } - - @InjectManager("baseRepository_") - async listPriceSetMoneyAmounts( - filters: PricingTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const records = await this.priceSetMoneyAmountService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountDTO[] - >(records, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCountPriceSetMoneyAmounts( - filters: PricingTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceSetMoneyAmountDTO[], number]> { - const [records, count] = - await this.priceSetMoneyAmountService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountDTO[] - >(records, { - populate: true, - }), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createPriceSetMoneyAmountRules( data: PricingTypes.CreatePriceSetMoneyAmountRulesDTO[], @@ -1211,7 +819,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< PricingTypes.PriceSetMoneyAmountRulesDTO[] >(records, { populate: true, @@ -1228,84 +836,13 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< PricingTypes.PriceSetMoneyAmountRulesDTO[] >(records, { populate: true, }) } - @InjectTransactionManager("baseRepository_") - async deletePriceSetMoneyAmountRules( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceSetMoneyAmountRulesService_.delete(ids, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceRule( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceRule = await this.priceRuleService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceRule, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceRules( - filters: PricingTypes.FilterablePriceRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceRules = await this.priceRuleService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceRules, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPriceRules( - filters: PricingTypes.FilterablePriceRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceRuleDTO[], number]> { - const [priceRules, count] = await this.priceRuleService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceRules, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createPriceRules( data: PricingTypes.CreatePriceRuleDTO[], @@ -1316,7 +853,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceRules, { populate: true, @@ -1331,7 +868,7 @@ export default class PricingModuleService< ): Promise { const priceRules = await this.priceRuleService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceRules, { populate: true, @@ -1339,77 +876,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deletePriceRules( - priceRuleIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceRuleService_.delete(priceRuleIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceList( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceList = await this.priceListService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceList, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceLists( - filters: PricingTypes.FilterablePriceListProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceLists = await this.priceListService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPriceLists( - filters: PricingTypes.FilterablePriceListProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceListDTO[], number]> { - const [priceLists, count] = await this.priceListService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ), - count, - ] - } - @InjectManager("baseRepository_") async createPriceLists( data: PricingTypes.CreatePriceListDTO[], @@ -1417,7 +883,7 @@ export default class PricingModuleService< ): Promise { const priceLists = await this.createPriceLists_(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1568,7 +1034,7 @@ export default class PricingModuleService< ): Promise { const priceLists = await this.updatePriceLists_(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1694,77 +1160,6 @@ export default class PricingModuleService< return updatedPriceLists } - @InjectTransactionManager("baseRepository_") - async deletePriceLists( - priceListIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceListService_.delete(priceListIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceListRule( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceList = await this.priceListRuleService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceList, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceListRules( - filters: PricingTypes.FilterablePriceListRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceLists = await this.priceListRuleService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPriceListRules( - filters: PricingTypes.FilterablePriceListRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceListRuleDTO[], number]> { - const [priceLists, count] = await this.priceListRuleService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ), - count, - ] - } - @InjectManager("baseRepository_") async createPriceListRules( data: PricingTypes.CreatePriceListRuleDTO[], @@ -1772,7 +1167,7 @@ export default class PricingModuleService< ): Promise { const priceLists = await this.createPriceListRules_(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1798,7 +1193,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1806,14 +1201,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deletePriceListRules( - priceListRuleIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceListRuleService_.delete(priceListRuleIds, sharedContext) - } - @InjectManager("baseRepository_") async addPriceListPrices( data: PricingTypes.AddPriceListPricesDTO[], @@ -2085,14 +1472,16 @@ export default class PricingModuleService< await Promise.all([ this.priceListRuleValueService_.delete( - priceListValuesToDelete.map((p) => p.id) + priceListValuesToDelete.map((p) => p.id), + sharedContext ), this.priceListRuleValueService_.create( - priceListRuleValuesToCreate as CreatePriceListRuleValueDTO[] + priceListRuleValuesToCreate as ServiceTypes.CreatePriceListRuleValueDTO[], + sharedContext ), ]) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -2154,7 +1543,7 @@ export default class PricingModuleService< await this.priceListRuleService_.delete(idsToDelete) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, diff --git a/packages/pricing/src/services/rule-type.ts b/packages/pricing/src/services/rule-type.ts index 9bd78752c0a6c..147754ad5a354 100644 --- a/packages/pricing/src/services/rule-type.ts +++ b/packages/pricing/src/services/rule-type.ts @@ -14,17 +14,9 @@ type InjectedDependencies = { export default class RuleTypeService< TEntity extends RuleType = RuleType -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateRuleTypeDTO - update: ServiceTypes.UpdateRuleTypeDTO - }, - { - list: ServiceTypes.FilterableRuleTypeProps - listAndCount: ServiceTypes.FilterableRuleTypeProps - } ->(RuleType) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + RuleType +) { protected readonly ruleTypeRepository_: DAL.RepositoryService constructor({ ruleTypeRepository }: InjectedDependencies) { @@ -33,21 +25,45 @@ export default class RuleTypeService< this.ruleTypeRepository_ = ruleTypeRepository } + create( + data: ServiceTypes.CreateRuleTypeDTO, + sharedContext: Context + ): Promise + create( + data: ServiceTypes.CreateRuleTypeDTO[], + sharedContext: Context + ): Promise + @InjectTransactionManager("ruleTypeRepository_") async create( - data: ServiceTypes.CreateRuleTypeDTO[], + data: ServiceTypes.CreateRuleTypeDTO | ServiceTypes.CreateRuleTypeDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { - validateRuleAttributes(data.map((d) => d.rule_attribute)) - return await this.ruleTypeRepository_.create(data, sharedContext) + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + validateRuleAttributes(data_.map((d) => d.rule_attribute)) + return await super.create(data, sharedContext) } + // @ts-ignore + update( + data: ServiceTypes.UpdateRuleTypeDTO[], + sharedContext: Context + ): Promise + // @ts-ignore + update( + data: ServiceTypes.UpdateRuleTypeDTO, + sharedContext: Context + ): Promise + @InjectTransactionManager("ruleTypeRepository_") + // TODO: add support for selector? and then rm ts ignore + // @ts-ignore async update( - data: ServiceTypes.UpdateRuleTypeDTO[], + data: ServiceTypes.UpdateRuleTypeDTO | ServiceTypes.UpdateRuleTypeDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { - validateRuleAttributes(data.map((d) => d.rule_attribute)) - return await this.ruleTypeRepository_.update(data, sharedContext) + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + validateRuleAttributes(data_.map((d) => d.rule_attribute)) + return await super.update(data, sharedContext) } } diff --git a/packages/pricing/src/types/repositories/index.ts b/packages/pricing/src/types/repositories/index.ts index 117d7bda46aa7..a1e262b975d75 100644 --- a/packages/pricing/src/types/repositories/index.ts +++ b/packages/pricing/src/types/repositories/index.ts @@ -1,44 +1,3 @@ -import { - Currency, - MoneyAmount, - PriceList, - PriceListRule, - PriceListRuleValue, - PriceRule, - PriceSet, - PriceSetMoneyAmount, - PriceSetMoneyAmountRules, - PriceSetRuleType, - RuleType, -} from "@models" -import { DAL } from "@medusajs/types" -import { CreateCurrencyDTO, UpdateCurrencyDTO } from "./currency" -import { CreateMoneyAmountDTO, UpdateMoneyAmountDTO } from "./money-amount" -import { - CreatePriceListRuleValueDTO, - UpdatePriceListRuleValueDTO, -} from "./price-list-rule-value" -import { - CreatePriceListRuleDTO, - UpdatePriceListRuleDTO, -} from "./price-list-rule" -import { CreatePriceListDTO, UpdatePriceListDTO } from "./price-list" -import { CreatePriceRuleDTO, UpdatePriceRuleDTO } from "./price-rule" -import { - CreatePriceSetMoneyAmountRulesDTO, - UpdatePriceSetMoneyAmountRulesDTO, -} from "./price-set-money-amount-rules" -import { - CreatePriceSetMoneyAmountDTO, - UpdatePriceSetMoneyAmountDTO, -} from "./price-set-money-amount" -import { - CreatePriceSetRuleTypeDTO, - UpdatePriceSetRuleTypeDTO, -} from "./price-set-rule-type" -import { CreatePriceSetDTO, UpdatePriceSetDTO } from "./price-set" -import { CreateRuleTypeDTO, UpdateRuleTypeDTO } from "./rule-type" - export * from "./currency" export * from "./money-amount" export * from "./price-list-rule-value" @@ -50,119 +9,3 @@ export * from "./price-set-money-amount" export * from "./price-set-rule-type" export * from "./price-set" export * from "./rule-type" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICurrencyRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateCurrencyDTO - update: UpdateCurrencyDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IMoneyAmountRepository< - TEntity extends MoneyAmount = MoneyAmount -> extends DAL.RepositoryService< - TEntity, - { - create: CreateMoneyAmountDTO - update: UpdateMoneyAmountDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceListRuleValueRepository< - TEntity extends PriceListRuleValue = PriceListRuleValue -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceListRuleValueDTO - update: UpdatePriceListRuleValueDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceListRuleRepository< - TEntity extends PriceListRule = PriceListRule -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceListRuleDTO - update: UpdatePriceListRuleDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceListRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceListDTO - update: UpdatePriceListDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceRuleRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceRuleDTO - update: UpdatePriceRuleDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetMoneyAmountRulesRepository< - TEntity extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetMoneyAmountRulesDTO - update: UpdatePriceSetMoneyAmountRulesDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetMoneyAmountRepository< - TEntity extends PriceSetMoneyAmount = PriceSetMoneyAmount -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetMoneyAmountDTO - update: UpdatePriceSetMoneyAmountDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetRuleTypeRepository< - TEntity extends PriceSetRuleType = PriceSetRuleType -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetRuleTypeDTO - update: UpdatePriceSetRuleTypeDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetDTO - update: UpdatePriceSetDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IRuleTypeRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateRuleTypeDTO - update: UpdateRuleTypeDTO - } - > {} diff --git a/packages/product/integration-tests/__tests__/services/product-collection/index.ts b/packages/product/integration-tests/__tests__/services/product-collection/index.ts index 98dc40be6c6e8..0f415c00c2728 100644 --- a/packages/product/integration-tests/__tests__/services/product-collection/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-collection/index.ts @@ -240,7 +240,7 @@ describe("Product collection Service", () => { error = e } - expect(error.message).toEqual('"productCollectionId" must be defined') + expect(error.message).toEqual("productCollection - id must be defined") }) it("should return collection based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts index 98bbc6418ae0c..e1d9506abdd43 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts @@ -796,7 +796,23 @@ describe("ProductModuleService products", function () { const products = await module.create([data]) + let retrievedProducts = await module.list({ id: products[0].id }) + + expect(retrievedProducts).toHaveLength(1) + expect(retrievedProducts[0].deleted_at).toBeNull() + await module.softDelete([products[0].id]) + + retrievedProducts = await module.list( + { id: products[0].id }, + { + withDeleted: true, + } + ) + + expect(retrievedProducts).toHaveLength(1) + expect(retrievedProducts[0].deleted_at).not.toBeNull() + await module.restore([products[0].id]) const deletedProducts = await module.list( diff --git a/packages/product/integration-tests/__tests__/services/product-option/index.ts b/packages/product/integration-tests/__tests__/services/product-option/index.ts index 3c0a26c0e4781..6f7e05b14e09a 100644 --- a/packages/product/integration-tests/__tests__/services/product-option/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-option/index.ts @@ -205,7 +205,7 @@ describe("ProductOption Service", () => { error = e } - expect(error.message).toEqual('"productOptionId" must be defined') + expect(error.message).toEqual("productOption - id must be defined") }) it("should return option based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-tag/index.ts b/packages/product/integration-tests/__tests__/services/product-tag/index.ts index c0a6ebe5571eb..178f40a7e6ce9 100644 --- a/packages/product/integration-tests/__tests__/services/product-tag/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-tag/index.ts @@ -237,7 +237,7 @@ describe("ProductTag Service", () => { error = e } - expect(error.message).toEqual('"productTagId" must be defined') + expect(error.message).toEqual("productTag - id must be defined") }) it("should return tag based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-type/index.ts b/packages/product/integration-tests/__tests__/services/product-type/index.ts index 9a878d308e6f0..da07bc92fb30c 100644 --- a/packages/product/integration-tests/__tests__/services/product-type/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-type/index.ts @@ -199,7 +199,7 @@ describe("ProductType Service", () => { error = e } - expect(error.message).toEqual('"productTypeId" must be defined') + expect(error.message).toEqual("productType - id must be defined") }) it("should return type based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-variant/index.ts b/packages/product/integration-tests/__tests__/services/product-variant/index.ts index 8f608d6dd8ecb..43c8229e01402 100644 --- a/packages/product/integration-tests/__tests__/services/product-variant/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-variant/index.ts @@ -320,7 +320,7 @@ describe("ProductVariant Service", () => { error = e } - expect(error.message).toEqual('"productVariantId" must be defined') + expect(error.message).toEqual("productVariant - id must be defined") }) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product/index.ts b/packages/product/integration-tests/__tests__/services/product/index.ts index c4aaaddb1c264..b64de6ae6f98d 100644 --- a/packages/product/integration-tests/__tests__/services/product/index.ts +++ b/packages/product/integration-tests/__tests__/services/product/index.ts @@ -78,7 +78,7 @@ describe("Product Service", () => { error = e } - expect(error.message).toEqual('"productId" must be defined') + expect(error.message).toEqual("product - id must be defined") }) it("should throw an error when product with id does not exist", async () => { @@ -217,7 +217,7 @@ describe("Product Service", () => { error = e } - expect(error.message).toEqual(`Product with id "undefined" not found`) + expect(error.message).toEqual(`Product with id "" not found`) let result = await service.retrieve(productOne.id) diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 2415a148cb909..5113c51c22168 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -144,7 +144,7 @@ class Product { @ManyToMany(() => ProductCategory, "products", { owner: true, pivotTable: "product_category_product", - cascade: ["soft-remove"] as any, + // TODO: rm cascade: ["soft-remove"] as any, }) categories = new Collection(this) diff --git a/packages/product/src/repositories/product-image.ts b/packages/product/src/repositories/product-image.ts index 90eb181c0c5f9..c37bbf7b438e9 100644 --- a/packages/product/src/repositories/product-image.ts +++ b/packages/product/src/repositories/product-image.ts @@ -14,6 +14,6 @@ export class ProductImageRepository extends DALUtils.mikroOrmBaseRepositoryFacto async upsert(urls: string[], context: Context = {}): Promise { const data = urls.map((url) => ({ url })) - return await super.upsert(data, context) + return (await super.upsert(data, context)) as Image[] } } diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts index eb430a06d4080..0ff5ebbd2ba92 100644 --- a/packages/product/src/repositories/product.ts +++ b/packages/product/src/repositories/product.ts @@ -17,8 +17,8 @@ import { DALUtils, isDefined, MedusaError, - promiseAll, ProductUtils, + promiseAll, } from "@medusajs/utils" import { ProductServiceTypes } from "../types/services" @@ -118,7 +118,10 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory[], + data: { + entity: Product + update: WithRequiredProperty + }[], context: Context = {} ): Promise { let categoryIds: string[] = [] @@ -128,7 +131,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(context) - data.forEach((productData) => { + data.forEach(({ update: productData }) => { categoryIds = categoryIds.concat( productData?.categories?.map((c) => c.id) || [] ) @@ -144,16 +147,6 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory updateData.id), - }, - { - populate: ["tags", "categories"], - } - ) - const collectionsToAssign = collectionIds.length ? await manager.find(ProductCollection, { id: collectionIds, @@ -195,11 +188,11 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( - productsToUpdate.map((product) => [product.id, product]) + data.map(({ entity }) => [entity.id, entity]) ) const products = await promiseAll( - data.map(async (updateData) => { + data.map(async ({ update: updateData }) => { const product = productsToUpdateMap.get(updateData.id) if (!product) { diff --git a/packages/product/src/services/__fixtures__/product.ts b/packages/product/src/services/__fixtures__/product.ts index ec32ccbb28646..7a185e0e24982 100644 --- a/packages/product/src/services/__fixtures__/product.ts +++ b/packages/product/src/services/__fixtures__/product.ts @@ -1,11 +1,8 @@ -import { asClass, asValue, createContainer } from "awilix" -import { ProductService } from "@services" +import { asValue } from "awilix" export const nonExistingProductId = "non-existing-id" -export const mockContainer = createContainer() -mockContainer.register({ - transaction: asValue(async (task) => await task()), +export const productRepositoryMock = { productRepository: asValue({ find: jest.fn().mockImplementation(async ({ where: { id } }) => { if (id === nonExistingProductId) { @@ -17,5 +14,4 @@ mockContainer.register({ findAndCount: jest.fn().mockResolvedValue([[], 0]), getFreshManager: jest.fn().mockResolvedValue({}), }), - productService: asClass(ProductService), -}) +} diff --git a/packages/product/src/services/__tests__/product.spec.ts b/packages/product/src/services/__tests__/product.spec.ts index 6d2820a19e9c7..fb1a6819d39d1 100644 --- a/packages/product/src/services/__tests__/product.spec.ts +++ b/packages/product/src/services/__tests__/product.spec.ts @@ -1,13 +1,28 @@ -import { mockContainer, nonExistingProductId } from "../__fixtures__/product" +import { + nonExistingProductId, + productRepositoryMock, +} from "../__fixtures__/product" +import { createMedusaContainer } from "@medusajs/utils" +import { asValue } from "awilix" +import ContainerLoader from "../../loaders/container" describe("Product service", function () { - beforeEach(function () { + let container + + beforeEach(async function () { jest.clearAllMocks() + + container = createMedusaContainer() + container.register("manager", asValue({})) + + await ContainerLoader({ container }) + + container.register(productRepositoryMock) }) it("should retrieve a product", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const productId = "existing-product" await productService.retrieve(productId) @@ -30,8 +45,8 @@ describe("Product service", function () { }) it("should fail to retrieve a product", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const err = await productService .retrieve(nonExistingProductId) @@ -59,8 +74,8 @@ describe("Product service", function () { }) it("should list products", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = {} const config = { @@ -85,8 +100,8 @@ describe("Product service", function () { }) it("should list products with filters", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = { tags: { @@ -123,8 +138,8 @@ describe("Product service", function () { }) it("should list products with filters and relations", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = { tags: { @@ -161,8 +176,8 @@ describe("Product service", function () { }) it("should list and count the products with filters and relations", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = { tags: { diff --git a/packages/product/src/services/index.ts b/packages/product/src/services/index.ts index dc485567aae5c..efc05645e7dcf 100644 --- a/packages/product/src/services/index.ts +++ b/packages/product/src/services/index.ts @@ -6,5 +6,3 @@ export { default as ProductTagService } from "./product-tag" export { default as ProductVariantService } from "./product-variant" export { default as ProductTypeService } from "./product-type" export { default as ProductOptionService } from "./product-option" -export { default as ProductImageService } from "./product-image" -export { default as ProductOptionValueService } from "./product-option-value" diff --git a/packages/product/src/services/product-collection.ts b/packages/product/src/services/product-collection.ts index 5ace64dea3783..4c51ece1e3a1c 100644 --- a/packages/product/src/services/product-collection.ts +++ b/packages/product/src/services/product-collection.ts @@ -7,14 +7,7 @@ import { } from "@medusajs/utils" import { ProductCollection } from "@models" -import { - IProductCollectionRepository, - ProductCollectionServiceTypes, -} from "@types" -import { - CreateProductCollection, - UpdateProductCollection, -} from "../types/services/product-collection" +import { ProductCollectionServiceTypes } from "@types" type InjectedDependencies = { productCollectionRepository: DAL.RepositoryService @@ -22,15 +15,11 @@ type InjectedDependencies = { export default class ProductCollectionService< TEntity extends ProductCollection = ProductCollection -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateProductCollection - update: UpdateProductCollection - } ->(ProductCollection) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductCollection +) { // eslint-disable-next-line max-len - protected readonly productCollectionRepository_: IProductCollectionRepository + protected readonly productCollectionRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) @@ -38,9 +27,9 @@ export default class ProductCollectionService< } @InjectManager("productCollectionRepository_") - async list( + async list( filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return await this.productCollectionRepository_.find( @@ -50,9 +39,9 @@ export default class ProductCollectionService< } @InjectManager("productCollectionRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return await this.productCollectionRepository_.findAndCount( @@ -61,11 +50,9 @@ export default class ProductCollectionService< ) } - protected buildListQueryOptions< - TEntityMethod = ProductTypes.ProductCollectionDTO - >( + protected buildListQueryOptions( filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) @@ -80,12 +67,24 @@ export default class ProductCollectionService< return queryOptions } + create( + data: ProductCollectionServiceTypes.CreateProductCollection, + context?: Context + ): Promise + create( + data: ProductCollectionServiceTypes.CreateProductCollection[], + context?: Context + ): Promise + @InjectTransactionManager("productCollectionRepository_") async create( - data: ProductCollectionServiceTypes.CreateProductCollection[], + data: + | ProductCollectionServiceTypes.CreateProductCollection + | ProductCollectionServiceTypes.CreateProductCollection[], context: Context = {} - ): Promise { - const productCollections = data.map((collectionData) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const productCollections = data_.map((collectionData) => { if (collectionData.product_ids) { collectionData.products = collectionData.product_ids @@ -98,12 +97,27 @@ export default class ProductCollectionService< return super.create(productCollections, context) } + // @ts-ignore + update( + data: ProductCollectionServiceTypes.UpdateProductCollection, + context?: Context + ): Promise + // @ts-ignore + update( + data: ProductCollectionServiceTypes.UpdateProductCollection[], + context?: Context + ): Promise + @InjectTransactionManager("productCollectionRepository_") + // @ts-ignore Do not implement all the expected overloads, see if we must do it async update( - data: ProductCollectionServiceTypes.UpdateProductCollection[], + data: + | ProductCollectionServiceTypes.UpdateProductCollection + | ProductCollectionServiceTypes.UpdateProductCollection[], context: Context = {} - ): Promise { - const productCollections = data.map((collectionData) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const productCollections = data_.map((collectionData) => { if (collectionData.product_ids) { collectionData.products = collectionData.product_ids diff --git a/packages/product/src/services/product-image.ts b/packages/product/src/services/product-image.ts deleted file mode 100644 index 50ab03044e0dc..0000000000000 --- a/packages/product/src/services/product-image.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Image } from "@models" -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" - -type InjectedDependencies = { - productImageRepository: DAL.RepositoryService -} - -export default class ProductImageService< - TEntity extends Image = Image -> extends ModulesSdkUtils.abstractServiceFactory( - Image -) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 6285fd47733b9..84febcf50eab2 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -2,13 +2,11 @@ import { Context, CreateProductOnlyDTO, DAL, - FindConfig, IEventBusModuleService, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, ProductTypes, - RestoreReturn, - SoftDeleteReturn, } from "@medusajs/types" import { Image, @@ -25,22 +23,12 @@ import { ProductCategoryService, ProductCollectionService, ProductOptionService, - ProductOptionValueService, ProductService, ProductTagService, ProductTypeService, ProductVariantService, } from "@services" -import ProductImageService from "./product-image" - -import { - ProductCategoryServiceTypes, - ProductCollectionServiceTypes, - ProductServiceTypes, - ProductVariantServiceTypes, -} from "@types" - import { arrayDifference, groupBy, @@ -49,25 +37,24 @@ import { isDefined, isString, kebabCase, - mapObjectTo, MedusaContext, MedusaError, + ModulesSdkUtils, promiseAll, } from "@medusajs/utils" +import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config" import { ProductEventData, ProductEvents } from "../types/services/product" import { ProductCategoryEventData, ProductCategoryEvents, } from "../types/services/product-category" import { - CreateProductOptionValueDTO, - UpdateProductOptionValueDTO, -} from "../types/services/product-option-value" -import { - entityNameToLinkableKeysMap, - joinerConfig, - LinkableKeys, -} from "./../joiner-config" + ProductCategoryServiceTypes, + ProductCollectionServiceTypes, + ProductOptionValueServiceTypes, + ProductServiceTypes, + ProductVariantServiceTypes, +} from "@types" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -76,24 +63,70 @@ type InjectedDependencies = { productTagService: ProductTagService productCategoryService: ProductCategoryService productCollectionService: ProductCollectionService - productImageService: ProductImageService + productImageService: ModulesSdkTypes.InternalModuleService productTypeService: ProductTypeService productOptionService: ProductOptionService - productOptionValueService: ProductOptionValueService + productOptionValueService: ModulesSdkTypes.InternalModuleService eventBusModuleService?: IEventBusModuleService } +const generateMethodForModels = [ + { model: ProductCategory, singular: "Category", plural: "Categories" }, + { model: ProductCollection, singular: "Collection", plural: "Collections" }, + { model: ProductOption, singular: "Option", plural: "Options" }, + { model: ProductTag, singular: "Tag", plural: "Tags" }, + { model: ProductType, singular: "Type", plural: "Types" }, + { model: ProductVariant, singular: "Variant", plural: "Variants" }, +] + export default class ProductModuleService< - TProduct extends Product = Product, - TProductVariant extends ProductVariant = ProductVariant, - TProductTag extends ProductTag = ProductTag, - TProductCollection extends ProductCollection = ProductCollection, - TProductCategory extends ProductCategory = ProductCategory, - TProductImage extends Image = Image, - TProductType extends ProductType = ProductType, - TProductOption extends ProductOption = ProductOption, - TProductOptionValue extends ProductOptionValue = ProductOptionValue -> implements ProductTypes.IProductModuleService + TProduct extends Product = Product, + TProductVariant extends ProductVariant = ProductVariant, + TProductTag extends ProductTag = ProductTag, + TProductCollection extends ProductCollection = ProductCollection, + TProductCategory extends ProductCategory = ProductCategory, + TProductImage extends Image = Image, + TProductType extends ProductType = ProductType, + TProductOption extends ProductOption = ProductOption, + TProductOptionValue extends ProductOptionValue = ProductOptionValue + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + ProductTypes.ProductDTO, + { + ProductCategory: { + dto: ProductTypes.ProductCategoryDTO + singular: "Category" + plural: "Categories" + } + ProductCollection: { + dto: ProductTypes.ProductCollectionDTO + singular: "Collection" + plural: "Collections" + } + ProductOption: { + dto: ProductTypes.ProductOptionDTO + singular: "Option" + plural: "Options" + } + ProductTag: { + dto: ProductTypes.ProductTagDTO + singular: "Tag" + plural: "Tags" + } + ProductType: { + dto: ProductTypes.ProductTypeDTO + singular: "Type" + plural: "Types" + } + ProductVariant: { + dto: ProductTypes.ProductVariantDTO + singular: "Variant" + plural: "Variants" + } + } + >(Product, generateMethodForModels, entityNameToLinkableKeysMap) + implements ProductTypes.IProductModuleService { protected baseRepository_: DAL.RepositoryService protected readonly productService_: ProductService @@ -107,11 +140,12 @@ export default class ProductModuleService< protected readonly productTagService_: ProductTagService // eslint-disable-next-line max-len protected readonly productCollectionService_: ProductCollectionService - protected readonly productImageService_: ProductImageService + // eslint-disable-next-line max-len + protected readonly productImageService_: ModulesSdkTypes.InternalModuleService protected readonly productTypeService_: ProductTypeService protected readonly productOptionService_: ProductOptionService // eslint-disable-next-line max-len - protected readonly productOptionValueService_: ProductOptionValueService + protected readonly productOptionValueService_: ModulesSdkTypes.InternalModuleService protected readonly eventBusModuleService_?: IEventBusModuleService constructor( @@ -130,6 +164,10 @@ export default class ProductModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.baseRepository_ = baseRepository this.productService_ = productService this.productVariantService_ = productVariantService @@ -147,96 +185,6 @@ export default class ProductModuleService< return joinerConfig } - @InjectManager("baseRepository_") - async list( - filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const products = await this.productService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(products)) - } - - @InjectManager("baseRepository_") - async retrieve( - productId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const product = await this.productService_.retrieve( - productId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(product)) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductDTO[], number]> { - const [products, count] = await this.productService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(products)), count] - } - - @InjectManager("baseRepository_") - async retrieveVariant( - productVariantId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productVariant = await this.productVariantService_.retrieve( - productVariantId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productVariant)) - } - - @InjectManager("baseRepository_") - async listVariants( - filters: ProductTypes.FilterableProductVariantProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const variants = await this.productVariantService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(variants)) - } - - @InjectManager("baseRepository_") - async listAndCountVariants( - filters: ProductTypes.FilterableProductVariantProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductVariantDTO[], number]> { - const [variants, count] = await this.productVariantService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(variants)), count] - } - async createVariants( data: ProductTypes.CreateProductVariantDTO[], @MedusaContext() sharedContext: Context = {} @@ -300,21 +248,6 @@ export default class ProductModuleService< return productVariants as unknown as ProductTypes.ProductVariantDTO[] } - @InjectTransactionManager("baseRepository_") - async deleteVariants( - productVariantIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productVariantService_.delete(productVariantIds, sharedContext) - - await this.eventBusModuleService_?.emit( - productVariantIds.map((id) => ({ - eventName: ProductEvents.PRODUCT_DELETED, - data: { id }, - })) - ) - } - @InjectManager("baseRepository_") async updateVariants( data: ProductTypes.UpdateProductVariantOnlyDTO[], @@ -324,9 +257,7 @@ export default class ProductModuleService< const updatedVariants = await this.baseRepository_.serialize< ProductTypes.ProductVariantDTO[] - >(productVariants, { - populate: true, - }) + >(productVariants) return updatedVariants } @@ -357,8 +288,8 @@ export default class ProductModuleService< } const optionValuesToUpsert: ( - | CreateProductOptionValueDTO - | UpdateProductOptionValueDTO + | ProductOptionValueServiceTypes.CreateProductOptionValueDTO + | ProductOptionValueServiceTypes.UpdateProductOptionValueDTO )[] = [] const optionsValuesToDelete: string[] = [] @@ -413,11 +344,7 @@ export default class ProductModuleService< const groups = groupBy(toUpdate, "product_id") - const [, , productVariants]: [ - void, - TProductOptionValue[], - TProductVariant[][] - ] = await promiseAll([ + const [, , productVariants] = await promiseAll([ await this.productOptionValueService_.delete( optionsValuesToDelete, sharedContext @@ -440,51 +367,6 @@ export default class ProductModuleService< return productVariants.flat() } - @InjectManager("baseRepository_") - async retrieveTag( - tagId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productTag = await this.productTagService_.retrieve( - tagId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productTag)) - } - - @InjectManager("baseRepository_") - async listTags( - filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const tags = await this.productTagService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(tags)) - } - - @InjectManager("baseRepository_") - async listAndCountTags( - filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductTagDTO[], number]> { - const [tags, count] = await this.productTagService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(tags)), count] - } - @InjectTransactionManager("baseRepository_") async createTags( data: ProductTypes.CreateProductTagDTO[], @@ -511,59 +393,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productTags)) } - @InjectTransactionManager("baseRepository_") - async deleteTags( - productTagIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productTagService_.delete(productTagIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveType( - typeId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productType = await this.productTypeService_.retrieve( - typeId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productType)) - } - - @InjectManager("baseRepository_") - async listTypes( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const types = await this.productTypeService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(types)) - } - - @InjectManager("baseRepository_") - async listAndCountTypes( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductTypeDTO[], number]> { - const [types, count] = await this.productTypeService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(types)), count] - } - @InjectTransactionManager("baseRepository_") async createTypes( data: ProductTypes.CreateProductTypeDTO[], @@ -590,60 +419,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productTypes)) } - @InjectTransactionManager("baseRepository_") - async deleteTypes( - productTypeIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productTypeService_.delete(productTypeIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveOption( - optionId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productOptions = await this.productOptionService_.retrieve( - optionId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productOptions)) - } - - @InjectManager("baseRepository_") - async listOptions( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productOptions = await this.productOptionService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productOptions)) - } - - @InjectManager("baseRepository_") - async listAndCountOptions( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductOptionDTO[], number]> { - const [productOptions, count] = - await this.productOptionService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(productOptions)), count] - } - @InjectTransactionManager("baseRepository_") async createOptions( data: ProductTypes.CreateProductOptionDTO[], @@ -656,9 +431,7 @@ export default class ProductModuleService< return await this.baseRepository_.serialize< ProductTypes.ProductOptionDTO[] - >(productOptions, { - populate: true, - }) + >(productOptions) } @InjectTransactionManager("baseRepository_") @@ -673,62 +446,7 @@ export default class ProductModuleService< return await this.baseRepository_.serialize< ProductTypes.ProductOptionDTO[] - >(productOptions, { - populate: true, - }) - } - - @InjectTransactionManager("baseRepository_") - async deleteOptions( - productOptionIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productOptionService_.delete(productOptionIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveCollection( - productCollectionId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productCollection = await this.productCollectionService_.retrieve( - productCollectionId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productCollection)) - } - - @InjectManager("baseRepository_") - async listCollections( - filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const collections = await this.productCollectionService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(collections)) - } - - @InjectManager("baseRepository_") - async listAndCountCollections( - filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductCollectionDTO[], number]> { - const collections = await this.productCollectionService_.listAndCount( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(collections)) + >(productOptions) } @InjectTransactionManager("baseRepository_") @@ -777,57 +495,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productCollections)) } - @InjectTransactionManager("baseRepository_") - async deleteCollections( - productCollectionIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productCollectionService_.delete( - productCollectionIds, - sharedContext - ) - - // eslint-disable-next-line max-len - await this.eventBusModuleService_?.emit( - productCollectionIds.map((id) => ({ - eventName: - ProductCollectionServiceTypes.ProductCollectionEvents - .COLLECTION_DELETED, - data: { id }, - })) - ) - } - - @InjectManager("baseRepository_") - async retrieveCategory( - productCategoryId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productCategory = await this.productCategoryService_.retrieve( - productCategoryId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productCategory)) - } - - @InjectManager("baseRepository_") - async listCategories( - filters: ProductTypes.FilterableProductCategoryProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const categories = await this.productCategoryService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(categories)) - } - @InjectTransactionManager("baseRepository_") async createCategory( data: ProductCategoryServiceTypes.CreateProductCategoryDTO, @@ -866,34 +533,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productCategory)) } - @InjectTransactionManager("baseRepository_") - async deleteCategory( - categoryId: string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productCategoryService_.delete(categoryId, sharedContext) - - await this.eventBusModuleService_?.emit( - ProductCategoryEvents.CATEGORY_DELETED, - { id: categoryId } - ) - } - - @InjectManager("baseRepository_") - async listAndCountCategories( - filters: ProductTypes.FilterableProductCategoryProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductCategoryDTO[], number]> { - const categories = await this.productCategoryService_.listAndCount( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(categories)) - } - @InjectManager("baseRepository_") async create( data: ProductTypes.CreateProductDTO[], @@ -902,9 +541,7 @@ export default class ProductModuleService< const products = await this.create_(data, sharedContext) const createdProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products, { - populate: true, - }) + >(products) await this.eventBusModuleService_?.emit( createdProducts.map(({ id }) => ({ @@ -925,9 +562,7 @@ export default class ProductModuleService< const updatedProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products, { - populate: true, - }) + >(products) await this.eventBusModuleService_?.emit( updatedProducts.map(({ id }) => ({ @@ -1297,131 +932,15 @@ export default class ProductModuleService< } @InjectTransactionManager("baseRepository_") - async delete( - productIds: string[], + async deleteCategory( + categoryId: string, @MedusaContext() sharedContext: Context = {} ): Promise { - await this.productService_.delete(productIds, sharedContext) - - await this.eventBusModuleService_?.emit( - productIds.map((id) => ({ - eventName: ProductEvents.PRODUCT_DELETED, - data: { id }, - })) - ) - } - - @InjectManager("baseRepository_") - async softDelete< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - productIds: string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [products, cascadedEntitiesMap] = await this.softDelete_( - productIds, - sharedContext - ) - - const softDeletedProducts = await this.baseRepository_.serialize< - ProductTypes.ProductDTO[] - >(products, { - populate: true, - }) - - await this.eventBusModuleService_?.emit( - softDeletedProducts.map(({ id }) => ({ - eventName: ProductEvents.PRODUCT_DELETED, - data: { id }, - })) - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - // Map internal table/column names to their respective external linkable keys - // eg: product.id = product_id, variant.id = variant_id - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - protected async softDelete_( - productIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TProduct[], Record]> { - return await this.productService_.softDelete(productIds, sharedContext) - } - - @InjectManager("baseRepository_") - async restore< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - productIds: string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.restore_( - productIds, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - // Map internal table/column names to their respective external linkable keys - // eg: product.id = product_id, variant.id = variant_id - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } + await this.productCategoryService_.delete(categoryId, sharedContext) - @InjectTransactionManager("baseRepository_") - async restoreVariants< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - variantIds: string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.productVariantService_.restore( - variantIds, - sharedContext + await this.eventBusModuleService_?.emit( + ProductCategoryEvents.CATEGORY_DELETED, + { id: categoryId } ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore_( - productIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TProduct[], Record]> { - return await this.productService_.restore(productIds, sharedContext) } } diff --git a/packages/product/src/services/product-option-value.ts b/packages/product/src/services/product-option-value.ts deleted file mode 100644 index 62c9bfa9f7a90..0000000000000 --- a/packages/product/src/services/product-option-value.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ProductOptionValue } from "@models" -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ProductOptionValueServiceTypes } from "@types" - -type InjectedDependencies = { - productOptionValueRepository: DAL.RepositoryService -} - -export default class ProductOptionValueService< - TEntity extends ProductOptionValue = ProductOptionValue -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductOptionValueServiceTypes.CreateProductOptionValueDTO - update: ProductOptionValueServiceTypes.UpdateProductOptionValueDTO - } ->(ProductOptionValue) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/product/src/services/product-option.ts b/packages/product/src/services/product-option.ts index 026c2a6b81f7b..eca5140fdefd2 100644 --- a/packages/product/src/services/product-option.ts +++ b/packages/product/src/services/product-option.ts @@ -1,7 +1,6 @@ import { ProductOption } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" -import { IProductOptionRepository } from "@types" type InjectedDependencies = { productOptionRepository: DAL.RepositoryService @@ -9,14 +8,10 @@ type InjectedDependencies = { export default class ProductOptionService< TEntity extends ProductOption = ProductOption -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductOptionDTO - update: ProductTypes.UpdateProductOptionDTO - } ->(ProductOption) { - protected readonly productOptionRepository_: IProductOptionRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductOption +) { + protected readonly productOptionRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) @@ -24,9 +19,9 @@ export default class ProductOptionService< } @InjectManager("productOptionRepository_") - async list( + async list( filters: ProductTypes.FilterableProductOptionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext?: Context ): Promise { return await this.productOptionRepository_.find( @@ -36,9 +31,9 @@ export default class ProductOptionService< } @InjectManager("productOptionRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductOptionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext?: Context ): Promise<[TEntity[], number]> { return await this.productOptionRepository_.findAndCount( @@ -47,9 +42,9 @@ export default class ProductOptionService< ) } - private buildQueryForList( + private buildQueryForList( filters: ProductTypes.FilterableProductOptionProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/product/src/services/product-tag.ts b/packages/product/src/services/product-tag.ts index a6d8522307826..2111a12862c27 100644 --- a/packages/product/src/services/product-tag.ts +++ b/packages/product/src/services/product-tag.ts @@ -1,7 +1,6 @@ import { ProductTag } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" -import { IProductTagRepository } from "@types" type InjectedDependencies = { productTagRepository: DAL.RepositoryService @@ -9,23 +8,19 @@ type InjectedDependencies = { export default class ProductTagService< TEntity extends ProductTag = ProductTag -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductTagDTO - update: ProductTypes.UpdateProductTagDTO - } ->(ProductTag) { - protected readonly productTagRepository_: IProductTagRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductTag +) { + protected readonly productTagRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) this.productTagRepository_ = container.productTagRepository } @InjectManager("productTagRepository_") - async list( + async list( filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return await this.productTagRepository_.find( @@ -35,9 +30,9 @@ export default class ProductTagService< } @InjectManager("productTagRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return await this.productTagRepository_.findAndCount( @@ -46,9 +41,9 @@ export default class ProductTagService< ) } - private buildQueryForList( + private buildQueryForList( filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/product/src/services/product-type.ts b/packages/product/src/services/product-type.ts index da5587f316dea..0c54344a916a5 100644 --- a/packages/product/src/services/product-type.ts +++ b/packages/product/src/services/product-type.ts @@ -1,7 +1,6 @@ import { ProductType } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" -import { IProductTypeRepository } from "@types" type InjectedDependencies = { productTypeRepository: DAL.RepositoryService @@ -9,14 +8,10 @@ type InjectedDependencies = { export default class ProductTypeService< TEntity extends ProductType = ProductType -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductTypeDTO - update: ProductTypes.UpdateProductTypeDTO - } ->(ProductType) { - protected readonly productTypeRepository_: IProductTypeRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductType +) { + protected readonly productTypeRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) @@ -24,9 +19,9 @@ export default class ProductTypeService< } @InjectManager("productTypeRepository_") - async list( + async list( filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return await this.productTypeRepository_.find( @@ -36,9 +31,9 @@ export default class ProductTypeService< } @InjectManager("productTypeRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return await this.productTypeRepository_.findAndCount( @@ -47,9 +42,9 @@ export default class ProductTypeService< ) } - private buildQueryForList( + private buildQueryForList( filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/product/src/services/product-variant.ts b/packages/product/src/services/product-variant.ts index e1a247aa1c8a6..823886728ec04 100644 --- a/packages/product/src/services/product-variant.ts +++ b/packages/product/src/services/product-variant.ts @@ -7,7 +7,7 @@ import { } from "@medusajs/utils" import { Product, ProductVariant } from "@models" -import { IProductVariantRepository, ProductVariantServiceTypes } from "@types" +import { ProductVariantServiceTypes } from "@types" import ProductService from "./product" type InjectedDependencies = { @@ -18,14 +18,10 @@ type InjectedDependencies = { export default class ProductVariantService< TEntity extends ProductVariant = ProductVariant, TProduct extends Product = Product -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductVariantOnlyDTO - update: ProductVariantServiceTypes.UpdateProductVariantDTO - } ->(ProductVariant) { - protected readonly productVariantRepository_: IProductVariantRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductVariant +) { + protected readonly productVariantRepository_: DAL.RepositoryService protected readonly productService_: ProductService constructor({ @@ -92,8 +88,6 @@ export default class ProductVariantService< const variantsData = [...data] variantsData.forEach((variant) => Object.assign(variant, { product })) - return await this.productVariantRepository_.update(variantsData, { - transactionManager: sharedContext.transactionManager, - }) + return await super.update(variantsData, sharedContext) } } diff --git a/packages/product/src/services/product.ts b/packages/product/src/services/product.ts index 3c514c0abea16..835f09b056bff 100644 --- a/packages/product/src/services/product.ts +++ b/packages/product/src/services/product.ts @@ -1,7 +1,6 @@ import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" import { Product } from "@models" -import { IProductRepository, ProductServiceTypes } from "@types" type InjectedDependencies = { productRepository: DAL.RepositoryService @@ -9,14 +8,10 @@ type InjectedDependencies = { export default class ProductService< TEntity extends Product = Product -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductOnlyDTO - update: ProductServiceTypes.UpdateProductDTO - } ->(Product) { - protected readonly productRepository_: IProductRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + Product +) { + protected readonly productRepository_: DAL.RepositoryService constructor({ productRepository }: InjectedDependencies) { // @ts-ignore @@ -27,9 +22,9 @@ export default class ProductService< } @InjectManager("productRepository_") - async list( + async list( filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { if (filters.category_id) { @@ -45,13 +40,13 @@ export default class ProductService< delete filters.category_id } - return await super.list(filters, config, sharedContext) + return await super.list(filters, config, sharedContext) } @InjectManager("productRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { if (filters.category_id) { @@ -67,10 +62,6 @@ export default class ProductService< delete filters.category_id } - return await super.listAndCount( - filters, - config, - sharedContext - ) + return await super.listAndCount(filters, config, sharedContext) } } diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts index 051d3d01a8d4f..2503374225c8e 100644 --- a/packages/product/src/types/index.ts +++ b/packages/product/src/types/index.ts @@ -6,4 +6,3 @@ export type InitializeModuleInjectableDependencies = { } export * from "./services" -export * from "./repositories" diff --git a/packages/product/src/types/repositories.ts b/packages/product/src/types/repositories.ts deleted file mode 100644 index 6f7f3a05108ef..0000000000000 --- a/packages/product/src/types/repositories.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { DAL, ProductTypes, WithRequiredProperty } from "@medusajs/types" -import { - Image, - Product, - ProductCollection, - ProductOption, - ProductOptionValue, - ProductTag, - ProductType, - ProductVariant, -} from "@models" -import { UpdateProductDTO } from "./services/product" -import { - CreateProductCollection, - UpdateProductCollection, -} from "./services/product-collection" -import { - CreateProductOptionValueDTO, - UpdateProductOptionValueDTO, -} from "./services/product-option-value" -import { UpdateProductVariantDTO } from "./services/product-variant" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductRepository - extends DAL.RepositoryService< - TEntity, - { - create: WithRequiredProperty - update: WithRequiredProperty - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductCollectionRepository< - TEntity extends ProductCollection = ProductCollection -> extends DAL.RepositoryService< - TEntity, - { - create: CreateProductCollection - update: UpdateProductCollection - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductImageRepository - extends DAL.RepositoryService {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductOptionRepository< - TEntity extends ProductOption = ProductOption -> extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductOptionDTO - update: ProductTypes.UpdateProductOptionDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductOptionValueRepository< - TEntity extends ProductOptionValue = ProductOptionValue -> extends DAL.RepositoryService< - TEntity, - { - create: CreateProductOptionValueDTO - update: UpdateProductOptionValueDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductTagRepository - extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductTagDTO - update: ProductTypes.UpdateProductTagDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductTypeRepository< - TEntity extends ProductType = ProductType -> extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductTypeDTO - update: ProductTypes.UpdateProductTypeDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductVariantRepository< - TEntity extends ProductVariant = ProductVariant -> extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductVariantOnlyDTO - update: UpdateProductVariantDTO - } - > {} diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts index 122cde64afbbb..a5d075f9fa451 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts @@ -55,7 +55,7 @@ describe("Promotion Module Service: Campaigns", () => { campaign_identifier: "test-1", starts_at: expect.any(Date), ends_at: expect.any(Date), - budget: expect.any(String), + budget: expect.any(Object), created_at: expect.any(Date), updated_at: expect.any(Date), deleted_at: null, @@ -68,7 +68,7 @@ describe("Promotion Module Service: Campaigns", () => { campaign_identifier: "test-2", starts_at: expect.any(Date), ends_at: expect.any(Date), - budget: expect.any(String), + budget: expect.any(Object), created_at: expect.any(Date), updated_at: expect.any(Date), deleted_at: null, @@ -378,7 +378,7 @@ describe("Promotion Module Service: Campaigns", () => { error = e } - expect(error.message).toEqual('"campaignId" must be defined') + expect(error.message).toEqual("campaign - id must be defined") }) it("should return campaign based on config select param", async () => { diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index dc36e066132d7..94f6953370364 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -890,7 +890,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should return promotion based on config select param", async () => { @@ -938,7 +938,7 @@ describe("Promotion Service", () => { campaign: null, is_automatic: false, type: "standard", - application_method: expect.any(String), + application_method: expect.any(Object), created_at: expect.any(Date), updated_at: expect.any(Date), deleted_at: null, @@ -1081,7 +1081,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create rules for a promotion", async () => { @@ -1156,7 +1156,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create target rules for a promotion", async () => { @@ -1246,7 +1246,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create buy rules for a buyget promotion", async () => { @@ -1334,7 +1334,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create rules for a promotion", async () => { @@ -1405,7 +1405,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create rules for a promotion", async () => { @@ -1489,7 +1489,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully remove rules for a promotion", async () => { diff --git a/packages/promotion/src/repositories/campaign.ts b/packages/promotion/src/repositories/campaign.ts index 231dcb8563351..afec080397274 100644 --- a/packages/promotion/src/repositories/campaign.ts +++ b/packages/promotion/src/repositories/campaign.ts @@ -4,13 +4,9 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { Campaign, Promotion } from "@models" import { CreateCampaignDTO, UpdateCampaignDTO } from "@types" -export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory< - Campaign, - { - create: CreateCampaignDTO - update: UpdateCampaignDTO - } ->(Campaign) { +export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory( + Campaign +) { async create( data: CreateCampaignDTO[], context: Context = {} @@ -64,7 +60,7 @@ export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory< } async update( - data: UpdateCampaignDTO[], + data: { entity: Campaign; update: UpdateCampaignDTO }[], context: Context = {} ): Promise { const manager = this.getActiveManager(context) @@ -72,7 +68,7 @@ export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory< const campaignIds: string[] = [] const campaignPromotionIdsMap = new Map() - data.forEach((campaignData) => { + data.forEach(({ update: campaignData }) => { const campaignPromotionIds = campaignData.promotions?.map((p) => p.id) campaignIds.push(campaignData.id) diff --git a/packages/promotion/src/services/application-method.ts b/packages/promotion/src/services/application-method.ts deleted file mode 100644 index 2f0e4d38be68a..0000000000000 --- a/packages/promotion/src/services/application-method.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ApplicationMethod } from "@models" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CreateApplicationMethodDTO, UpdateApplicationMethodDTO } from "@types" - -type InjectedDependencies = { - applicationMethodRepository: DAL.RepositoryService -} - -export default class ApplicationMethodService< - TEntity extends ApplicationMethod = ApplicationMethod -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateApplicationMethodDTO - update: UpdateApplicationMethodDTO - }, - { - list: PromotionTypes.FilterableApplicationMethodProps - listAndCount: PromotionTypes.FilterableApplicationMethodProps - } ->(ApplicationMethod) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/campaign-budget.ts b/packages/promotion/src/services/campaign-budget.ts deleted file mode 100644 index 269b337787e57..0000000000000 --- a/packages/promotion/src/services/campaign-budget.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CampaignBudget } from "@models" -import { CreateCampaignBudgetDTO, UpdateCampaignBudgetDTO } from "../types" - -type InjectedDependencies = { - campaignBudgetRepository: DAL.RepositoryService -} - -export default class CampaignBudgetService< - TEntity extends CampaignBudget = CampaignBudget -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCampaignBudgetDTO - update: UpdateCampaignBudgetDTO - }, - { - list: PromotionTypes.FilterableCampaignBudgetProps - listAndCount: PromotionTypes.FilterableCampaignBudgetProps - } ->(CampaignBudget) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/campaign.ts b/packages/promotion/src/services/campaign.ts deleted file mode 100644 index 1f7b83adae2d1..0000000000000 --- a/packages/promotion/src/services/campaign.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Campaign } from "@models" -import { CreateCampaignDTO, UpdateCampaignDTO } from "../types" - -type InjectedDependencies = { - campaignRepository: DAL.RepositoryService -} - -export default class CampaignService< - TEntity extends Campaign = Campaign -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCampaignDTO - update: UpdateCampaignDTO - }, - { - list: PromotionTypes.FilterableCampaignProps - listAndCount: PromotionTypes.FilterableCampaignProps - } ->(Campaign) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/index.ts b/packages/promotion/src/services/index.ts index dcd93f46b8fad..c127ea20f4bcf 100644 --- a/packages/promotion/src/services/index.ts +++ b/packages/promotion/src/services/index.ts @@ -1,7 +1 @@ -export { default as ApplicationMethodService } from "./application-method" -export { default as CampaignService } from "./campaign" -export { default as CampaignBudgetService } from "./campaign-budget" -export { default as PromotionService } from "./promotion" export { default as PromotionModuleService } from "./promotion-module" -export { default as PromotionRuleService } from "./promotion-rule" -export { default as PromotionRuleValueService } from "./promotion-rule-value" diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index 158e8201c6847..2fc707942a92e 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -1,23 +1,21 @@ import { Context, DAL, - FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, PromotionTypes, - RestoreReturn, - SoftDeleteReturn, } from "@medusajs/types" import { ApplicationMethodTargetType, CampaignBudgetType, InjectManager, InjectTransactionManager, + isString, MedusaContext, MedusaError, + ModulesSdkUtils, PromotionType, - isString, - mapObjectTo, } from "@medusajs/utils" import { ApplicationMethod, @@ -27,14 +25,6 @@ import { PromotionRule, PromotionRuleValue, } from "@models" -import { - ApplicationMethodService, - CampaignBudgetService, - CampaignService, - PromotionRuleService, - PromotionRuleValueService, - PromotionService, -} from "@services" import { ApplicationMethodRuleTypes, CreateApplicationMethodDTO, @@ -48,43 +38,60 @@ import { UpdatePromotionDTO, } from "@types" import { - ComputeActionUtils, allowedAllocationForQuantity, areRulesValidForContext, + ComputeActionUtils, validateApplicationMethodAttributes, validatePromotionRuleAttributes, } from "@utils" -import { - LinkableKeys, - entityNameToLinkableKeysMap, - joinerConfig, -} from "../joiner-config" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - promotionService: PromotionService - applicationMethodService: ApplicationMethodService - promotionRuleService: PromotionRuleService - promotionRuleValueService: PromotionRuleValueService - campaignService: CampaignService - campaignBudgetService: CampaignBudgetService + promotionService: ModulesSdkTypes.InternalModuleService + applicationMethodService: ModulesSdkTypes.InternalModuleService + promotionRuleService: ModulesSdkTypes.InternalModuleService + promotionRuleValueService: ModulesSdkTypes.InternalModuleService + campaignService: ModulesSdkTypes.InternalModuleService + campaignBudgetService: ModulesSdkTypes.InternalModuleService } +const generateMethodForModels = [ + ApplicationMethod, + Campaign, + CampaignBudget, + PromotionRule, + PromotionRuleValue, +] + export default class PromotionModuleService< - TPromotion extends Promotion = Promotion, - TPromotionRule extends PromotionRule = PromotionRule, - TPromotionRuleValue extends PromotionRuleValue = PromotionRuleValue, - TCampaign extends Campaign = Campaign, - TCampaignBudget extends CampaignBudget = CampaignBudget -> implements PromotionTypes.IPromotionModuleService + TApplicationMethod extends ApplicationMethod = ApplicationMethod, + TPromotion extends Promotion = Promotion, + TPromotionRule extends PromotionRule = PromotionRule, + TPromotionRuleValue extends PromotionRuleValue = PromotionRuleValue, + TCampaign extends Campaign = Campaign, + TCampaignBudget extends CampaignBudget = CampaignBudget + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + PromotionTypes.PromotionDTO, + { + ApplicationMethod: { dto: PromotionTypes.ApplicationMethodDTO } + Campaign: { dto: PromotionTypes.CampaignDTO } + CampaignBudget: { dto: PromotionTypes.CampaignBudgetDTO } + PromotionRule: { dto: PromotionTypes.PromotionRuleDTO } + PromotionRuleValue: { dto: PromotionTypes.PromotionRuleValueDTO } + } + >(Promotion, generateMethodForModels, entityNameToLinkableKeysMap) + implements PromotionTypes.IPromotionModuleService { protected baseRepository_: DAL.RepositoryService - protected promotionService_: PromotionService - protected applicationMethodService_: ApplicationMethodService - protected promotionRuleService_: PromotionRuleService - protected promotionRuleValueService_: PromotionRuleValueService - protected campaignService_: CampaignService - protected campaignBudgetService_: CampaignBudgetService + protected promotionService_: ModulesSdkTypes.InternalModuleService + protected applicationMethodService_: ModulesSdkTypes.InternalModuleService + protected promotionRuleService_: ModulesSdkTypes.InternalModuleService + protected promotionRuleValueService_: ModulesSdkTypes.InternalModuleService + protected campaignService_: ModulesSdkTypes.InternalModuleService + protected campaignBudgetService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -98,6 +105,9 @@ export default class PromotionModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.promotionService_ = promotionService this.applicationMethodService_ = applicationMethodService @@ -399,63 +409,6 @@ export default class PromotionModuleService< return computedActions } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const promotion = await this.promotionService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - promotion, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async list( - filters: PromotionTypes.FilterablePromotionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const promotions = await this.promotionService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - promotions, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: PromotionTypes.FilterablePromotionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PromotionTypes.PromotionDTO[], number]> { - const [promotions, count] = await this.promotionService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - promotions, - { populate: true } - ), - count, - ] - } - async create( data: PromotionTypes.CreatePromotionDTO, sharedContext?: Context @@ -921,88 +874,6 @@ export default class PromotionModuleService< } } - @InjectTransactionManager("baseRepository_") - async delete( - ids: string[] | string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - - await this.promotionService_.delete(idsToDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async softDelete< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string | string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - let [_, cascadedEntitiesMap] = await this.softDelete_( - idsToDelete, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - protected async softDelete_( - promotionIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TPromotion[], Record]> { - return await this.promotionService_.softDelete(promotionIds, sharedContext) - } - - @InjectManager("baseRepository_") - async restore< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string | string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToRestore = Array.isArray(ids) ? ids : [ids] - const [_, cascadedEntitiesMap] = await this.restore_( - idsToRestore, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore_( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TPromotion[], Record]> { - return await this.promotionService_.restore(ids, sharedContext) - } - @InjectManager("baseRepository_") async removePromotionRules( promotionId: string, @@ -1134,63 +1005,6 @@ export default class PromotionModuleService< ) } - @InjectManager("baseRepository_") - async retrieveCampaign( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const campaign = await this.campaignService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - campaign, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listCampaigns( - filters: PromotionTypes.FilterableCampaignProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const campaigns = await this.campaignService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - campaigns, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCountCampaigns( - filters: PromotionTypes.FilterableCampaignProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PromotionTypes.CampaignDTO[], number]> { - const [campaigns, count] = await this.campaignService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - campaigns, - { populate: true } - ), - count, - ] - } - async createCampaigns( data: PromotionTypes.CreateCampaignDTO, sharedContext?: Context @@ -1361,82 +1175,4 @@ export default class PromotionModuleService< return updatedCampaigns } - - @InjectTransactionManager("baseRepository_") - async deleteCampaigns( - ids: string | string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - - await this.campaignService_.delete(idsToDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async softDeleteCampaigns( - ids: string | string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - let [_, cascadedEntitiesMap] = await this.softDeleteCampaigns_( - idsToDelete, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - protected async softDeleteCampaigns_( - campaignIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TCampaign[], Record]> { - return await this.campaignService_.softDelete(campaignIds, sharedContext) - } - - @InjectManager("baseRepository_") - async restoreCampaigns< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string | string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToRestore = Array.isArray(ids) ? ids : [ids] - const [_, cascadedEntitiesMap] = await this.restoreCampaigns_( - idsToRestore, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restoreCampaigns_( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TCampaign[], Record]> { - return await this.campaignService_.restore(ids, sharedContext) - } } diff --git a/packages/promotion/src/services/promotion-rule-value.ts b/packages/promotion/src/services/promotion-rule-value.ts deleted file mode 100644 index 90a0487d94685..0000000000000 --- a/packages/promotion/src/services/promotion-rule-value.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PromotionRuleValue } from "@models" -import { - CreatePromotionRuleValueDTO, - UpdatePromotionRuleValueDTO, -} from "../types" - -type InjectedDependencies = { - promotionRuleValueRepository: DAL.RepositoryService -} - -export default class PromotionRuleValueService< - TEntity extends PromotionRuleValue = PromotionRuleValue -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePromotionRuleValueDTO - update: UpdatePromotionRuleValueDTO - }, - { - list: PromotionTypes.FilterablePromotionRuleValueProps - listAndCount: PromotionTypes.FilterablePromotionRuleValueProps - } ->(PromotionRuleValue) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/promotion-rule.ts b/packages/promotion/src/services/promotion-rule.ts deleted file mode 100644 index a8654617c7c5d..0000000000000 --- a/packages/promotion/src/services/promotion-rule.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PromotionRule } from "@models" -import { CreatePromotionRuleDTO, UpdatePromotionRuleDTO } from "../types" - -type InjectedDependencies = { - promotionRuleRepository: DAL.RepositoryService -} - -export default class PromotionRuleService< - TEntity extends PromotionRule = PromotionRule -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePromotionRuleDTO - update: UpdatePromotionRuleDTO - }, - { - list: PromotionTypes.FilterablePromotionRuleProps - listAndCount: PromotionTypes.FilterablePromotionRuleProps - } ->(PromotionRule) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/promotion.ts b/packages/promotion/src/services/promotion.ts deleted file mode 100644 index 74c8e71ba3274..0000000000000 --- a/packages/promotion/src/services/promotion.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Promotion } from "@models" -import { CreatePromotionDTO, UpdatePromotionDTO } from "../types" - -type InjectedDependencies = { - promotionRepository: DAL.RepositoryService -} - -export default class PromotionService< - TEntity extends Promotion = Promotion -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePromotionDTO - update: UpdatePromotionDTO - }, - { - list: PromotionTypes.FilterablePromotionProps - listAndCount: PromotionTypes.FilterablePromotionProps - } ->(Promotion) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/types/index.ts b/packages/promotion/src/types/index.ts index 80891678e3717..145c717bae997 100644 --- a/packages/promotion/src/types/index.ts +++ b/packages/promotion/src/types/index.ts @@ -10,4 +10,3 @@ export * from "./campaign-budget" export * from "./promotion" export * from "./promotion-rule" export * from "./promotion-rule-value" -export * from "./repositories" diff --git a/packages/promotion/src/types/repositories.ts b/packages/promotion/src/types/repositories.ts deleted file mode 100644 index bfa06a3073535..0000000000000 --- a/packages/promotion/src/types/repositories.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - ApplicationMethod, - Campaign, - CampaignBudget, - Promotion, - PromotionRule, - PromotionRuleValue, -} from "@models" -import { DAL } from "@medusajs/types" -import { - CreateApplicationMethodDTO, - UpdateApplicationMethodDTO, -} from "./application-method" -import { CreateCampaignDTO, UpdateCampaignDTO } from "./campaign" -import { - CreateCampaignBudgetDTO, - UpdateCampaignBudgetDTO, -} from "./campaign-budget" -import { CreatePromotionDTO, UpdatePromotionDTO } from "./promotion" -import { - CreatePromotionRuleDTO, - UpdatePromotionRuleDTO, -} from "./promotion-rule" -import { - CreatePromotionRuleValueDTO, - UpdatePromotionRuleValueDTO, -} from "./promotion-rule-value" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IApplicationMethodRepository< - TEntity extends ApplicationMethod = ApplicationMethod -> extends DAL.RepositoryService< - TEntity, - { - create: CreateApplicationMethodDTO - update: UpdateApplicationMethodDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICampaignRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateCampaignDTO - update: UpdateCampaignDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICampaignBudgetRepository< - TEntity extends CampaignBudget = CampaignBudget -> extends DAL.RepositoryService< - TEntity, - { - create: CreateCampaignBudgetDTO - update: UpdateCampaignBudgetDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPromotionRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePromotionDTO - Update: UpdatePromotionDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPromotionRuleRepository< - TEntity extends PromotionRule = PromotionRule -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePromotionRuleDTO - update: UpdatePromotionRuleDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPromotionRuleValueRepository< - TEntity extends PromotionRuleValue = PromotionRuleValue -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePromotionRuleValueDTO - update: UpdatePromotionRuleValueDTO - } - > {} diff --git a/packages/sales-channel/src/services/__fixtures__/sales-channel.ts b/packages/sales-channel/src/services/__fixtures__/sales-channel.ts index c4a716dde4ad6..4549d004b4080 100644 --- a/packages/sales-channel/src/services/__fixtures__/sales-channel.ts +++ b/packages/sales-channel/src/services/__fixtures__/sales-channel.ts @@ -1,10 +1,6 @@ -import { SalesChannelService, SalesChannelModuleService } from "@services" -import { asClass, asValue, createContainer } from "awilix" +import { asValue } from "awilix" -export const mockContainer = createContainer() - -mockContainer.register({ - transaction: asValue(async (task) => await task()), +export const salesChannelRepositoryMock = { salesChannelRepository: asValue({ find: jest.fn().mockImplementation(async ({ where: { code } }) => { return [{}] @@ -12,6 +8,4 @@ mockContainer.register({ findAndCount: jest.fn().mockResolvedValue([[], 0]), getFreshManager: jest.fn().mockResolvedValue({}), }), - salesChannelService: asClass(SalesChannelService), - salesChannelModuleService: asClass(SalesChannelModuleService), -}) +} diff --git a/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts b/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts index d8dab120bdf96..61e5bb1a1f4ff 100644 --- a/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts +++ b/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts @@ -1,15 +1,25 @@ -import { mockContainer } from "../__fixtures__/sales-channel" +import { createMedusaContainer } from "@medusajs/utils" +import { asValue } from "awilix" +import ContainerLoader from "../../loaders/container" +import { salesChannelRepositoryMock } from "../__fixtures__/sales-channel" describe("Sales channel service", function () { - beforeEach(function () { + let container + + beforeEach(async function () { jest.clearAllMocks() + + container = createMedusaContainer() + container.register("manager", asValue({})) + + await ContainerLoader({ container }) + + container.register(salesChannelRepositoryMock) }) it("should list sales channels with filters and relations", async function () { - const salesChannelRepository = mockContainer.resolve( - "salesChannelRepository" - ) - const salesChannelService = mockContainer.resolve("salesChannelService") + const salesChannelRepository = container.resolve("salesChannelRepository") + const salesChannelService = container.resolve("salesChannelService") const config = { select: ["id", "name"], diff --git a/packages/sales-channel/src/services/index.ts b/packages/sales-channel/src/services/index.ts index 117f8a36c6b86..d00ec9ecfc719 100644 --- a/packages/sales-channel/src/services/index.ts +++ b/packages/sales-channel/src/services/index.ts @@ -1,2 +1 @@ -export { default as SalesChannelService } from "./sales-channel" export { default as SalesChannelModuleService } from "./sales-channel-module" diff --git a/packages/sales-channel/src/services/sales-channel-module.ts b/packages/sales-channel/src/services/sales-channel-module.ts index 270629f7c67a4..b9d02de4eaa58 100644 --- a/packages/sales-channel/src/services/sales-channel-module.ts +++ b/packages/sales-channel/src/services/sales-channel-module.ts @@ -1,48 +1,48 @@ import { Context, + CreateSalesChannelDTO, DAL, - FilterableSalesChannelProps, - FindConfig, InternalModuleDeclaration, ISalesChannelModuleService, ModuleJoinerConfig, - RestoreReturn, + ModulesSdkTypes, SalesChannelDTO, - SoftDeleteReturn, + UpdateSalesChannelDTO, } from "@medusajs/types" import { - InjectManager, InjectTransactionManager, - mapObjectTo, MedusaContext, + ModulesSdkUtils, } from "@medusajs/utils" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" import { SalesChannel } from "@models" -import SalesChannelService from "./sales-channel" -import { - joinerConfig, - entityNameToLinkableKeysMap, - LinkableKeys, -} from "../joiner-config" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - salesChannelService: SalesChannelService + salesChannelService: ModulesSdkTypes.InternalModuleService } export default class SalesChannelModuleService< - TEntity extends SalesChannel = SalesChannel -> implements ISalesChannelModuleService + TEntity extends SalesChannel = SalesChannel + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + SalesChannelDTO, + {} + >(SalesChannel, [], entityNameToLinkableKeysMap) + implements ISalesChannelModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly salesChannelService_: SalesChannelService + protected readonly salesChannelService_: ModulesSdkTypes.InternalModuleService constructor( { baseRepository, salesChannelService }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) this.baseRepository_ = baseRepository this.salesChannelService_ = salesChannelService } @@ -78,95 +78,6 @@ export default class SalesChannelModuleService< ) } - async delete(ids: string[], sharedContext?: Context): Promise - - async delete(id: string, sharedContext?: Context): Promise - - @InjectTransactionManager("baseRepository_") - async delete( - ids: string | string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - const salesChannelIds = Array.isArray(ids) ? ids : [ids] - await this.salesChannelService_.delete(salesChannelIds, sharedContext) - } - - @InjectTransactionManager("baseRepository_") - protected async softDelete_( - salesChannelIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this.salesChannelService_.softDelete( - salesChannelIds, - sharedContext - ) - } - - @InjectManager("baseRepository_") - async softDelete< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - salesChannelIds: string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.softDelete_( - salesChannelIds, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore_( - salesChannelIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this.salesChannelService_.restore( - salesChannelIds, - sharedContext - ) - } - - @InjectManager("baseRepository_") - async restore< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - salesChannelIds: string[], - { returnLinkableKeys }: RestoreReturn = {}, - sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.restore_( - salesChannelIds, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - async update( data: UpdateSalesChannelDTO[], sharedContext?: Context @@ -193,55 +104,4 @@ export default class SalesChannelModuleService< } ) } - - @InjectManager("baseRepository_") - async retrieve( - salesChannelId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const salesChannel = await this.salesChannelService_.retrieve( - salesChannelId, - config - ) - - return await this.baseRepository_.serialize(salesChannel, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async list( - filters: {} = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const salesChannels = await this.salesChannelService_.list(filters, config) - - return await this.baseRepository_.serialize( - salesChannels, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: FilterableSalesChannelProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[SalesChannelDTO[], number]> { - const [salesChannels, count] = await this.salesChannelService_.listAndCount( - filters, - config - ) - - return [ - await this.baseRepository_.serialize(salesChannels, { - populate: true, - }), - count, - ] - } } diff --git a/packages/sales-channel/src/services/sales-channel.ts b/packages/sales-channel/src/services/sales-channel.ts deleted file mode 100644 index 83e6ef90b4bcd..0000000000000 --- a/packages/sales-channel/src/services/sales-channel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" - -import { SalesChannel } from "@models" - -type InjectedDependencies = { - salesChannelRepository: DAL.RepositoryService -} - -export default class SalesChannelService< - TEntity extends SalesChannel = SalesChannel -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateSalesChannelDTO - update: UpdateSalesChannelDTO - } ->(SalesChannel) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/sales-channel/src/types/repositories.ts b/packages/sales-channel/src/types/repositories.ts deleted file mode 100644 index e1a90fef2194c..0000000000000 --- a/packages/sales-channel/src/types/repositories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DAL } from "@medusajs/types" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -import { SalesChannel } from "@models" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" - -export interface ISalesChannelRepository< - TEntity extends SalesChannel = SalesChannel -> extends DAL.RepositoryService< - TEntity, - { - create: CreateSalesChannelDTO - update: UpdateSalesChannelDTO - } - > {} diff --git a/packages/types/src/auth/common/auth-provider.ts b/packages/types/src/auth/common/auth-provider.ts index 3f4122d42f4cd..ddf833879748f 100644 --- a/packages/types/src/auth/common/auth-provider.ts +++ b/packages/types/src/auth/common/auth-provider.ts @@ -25,6 +25,7 @@ export type UpdateAuthProviderDTO = { export interface FilterableAuthProviderProps extends BaseFilterable { + id?: string | string[] provider?: string[] is_active?: boolean scope?: string[] diff --git a/packages/types/src/auth/service.ts b/packages/types/src/auth/service.ts index 88b72362db24b..1dd4c4ecc2cc2 100644 --- a/packages/types/src/auth/service.ts +++ b/packages/types/src/auth/service.ts @@ -68,7 +68,7 @@ export interface IAuthModuleService extends IModuleService { sharedContext?: Context ): Promise - deleteAuthProvider(ids: string[], sharedContext?: Context): Promise + deleteAuthProviders(ids: string[], sharedContext?: Context): Promise retrieveAuthUser( id: string, @@ -118,5 +118,5 @@ export interface IAuthModuleService extends IModuleService { sharedContext?: Context ): Promise - deleteAuthUser(ids: string[], sharedContext?: Context): Promise + deleteAuthUsers(ids: string[], sharedContext?: Context): Promise } diff --git a/packages/types/src/common/common.ts b/packages/types/src/common/common.ts index 99c3dd9367f26..d97ebd31fb8bc 100644 --- a/packages/types/src/common/common.ts +++ b/packages/types/src/common/common.ts @@ -4,11 +4,11 @@ import { FindOperator, FindOptionsSelect, FindOptionsWhere, - OrderByCondition -} from "typeorm"; + OrderByCondition, +} from "typeorm" -import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder"; -import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations"; +import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" +import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations" /** * Utility type used to remove some optional attributes (coming from K) from a type T @@ -230,3 +230,17 @@ export interface FindPaginationParams { offset?: number limit?: number } + +export type Pluralize = Singular extends `${infer R}y` + ? `${R}ies` + : Singular extends `${infer R}es` + ? `${Singular}` + : Singular extends + | `${infer R}ss` + | `${infer R}sh` + | `${infer R}ch` + | `${infer R}x` + | `${infer R}z` + | `${infer R}o` + ? `${Singular}es` + : `${Singular}s` diff --git a/packages/types/src/customer/service.ts b/packages/types/src/customer/service.ts index 8143bd213a22e..b2c348ffe03e5 100644 --- a/packages/types/src/customer/service.ts +++ b/packages/types/src/customer/service.ts @@ -3,15 +3,15 @@ import { RestoreReturn, SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { + CustomerAddressDTO, CustomerDTO, - CustomerGroupDTO, CustomerGroupCustomerDTO, + CustomerGroupDTO, + FilterableCustomerAddressProps, FilterableCustomerGroupCustomerProps, - FilterableCustomerProps, FilterableCustomerGroupProps, + FilterableCustomerProps, GroupCustomerPair, - FilterableCustomerAddressProps, - CustomerAddressDTO, } from "./common" import { CreateCustomerAddressDTO, @@ -58,11 +58,13 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise + // TODO should be pluralized createCustomerGroup( data: CreateCustomerGroupDTO[], sharedContext?: Context ): Promise + // TODO should be pluralized createCustomerGroup( data: CreateCustomerGroupDTO, sharedContext?: Context @@ -74,28 +76,30 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise - updateCustomerGroup( + updateCustomerGroups( groupId: string, data: CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - updateCustomerGroup( + + updateCustomerGroups( groupIds: string[], data: CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - updateCustomerGroup( + + updateCustomerGroups( selector: FilterableCustomerGroupProps, data: CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - deleteCustomerGroup(groupId: string, sharedContext?: Context): Promise - deleteCustomerGroup( + deleteCustomerGroups(groupId: string, sharedContext?: Context): Promise + deleteCustomerGroups( groupIds: string[], sharedContext?: Context ): Promise - deleteCustomerGroup( + deleteCustomerGroups( selector: FilterableCustomerGroupProps, sharedContext?: Context ): Promise @@ -110,10 +114,13 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise<{ id: string }[]> + // TODO should be pluralized removeCustomerFromGroup( groupCustomerPair: GroupCustomerPair, sharedContext?: Context ): Promise + + // TODO should be pluralized removeCustomerFromGroup( groupCustomerPairs: GroupCustomerPair[], sharedContext?: Context @@ -128,25 +135,26 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise - updateAddress( + updateAddresses( addressId: string, data: UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - updateAddress( + updateAddresses( addressIds: string[], data: UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - updateAddress( + + updateAddresses( selector: FilterableCustomerAddressProps, data: UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - deleteAddress(addressId: string, sharedContext?: Context): Promise - deleteAddress(addressIds: string[], sharedContext?: Context): Promise - deleteAddress( + deleteAddresses(addressId: string, sharedContext?: Context): Promise + deleteAddresses(addressIds: string[], sharedContext?: Context): Promise + deleteAddresses( selector: FilterableCustomerAddressProps, sharedContext?: Context ): Promise @@ -163,7 +171,7 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise<[CustomerAddressDTO[], number]> - listCustomerGroupRelations( + listCustomerGroupCustomers( filters?: FilterableCustomerGroupCustomerProps, config?: FindConfig, sharedContext?: Context @@ -205,13 +213,13 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise | void> - softDeleteCustomerGroup( + softDeleteCustomerGroups( groupIds: string[], config?: SoftDeleteReturn, sharedContext?: Context ): Promise | void> - restoreCustomerGroup( + restoreCustomerGroups( groupIds: string[], config?: RestoreReturn, sharedContext?: Context diff --git a/packages/types/src/dal/repository-service.ts b/packages/types/src/dal/repository-service.ts index cc18daff1aca3..a2f5339e8b60c 100644 --- a/packages/types/src/dal/repository-service.ts +++ b/packages/types/src/dal/repository-service.ts @@ -1,6 +1,11 @@ import { RepositoryTransformOptions } from "../common" import { Context } from "../shared-context" -import { FindOptions } from "./index" +import { + BaseFilterable, + FilterQuery as InternalFilterQuery, + FilterQuery, + FindOptions, +} from "./index" /** * Data access layer (DAL) interface to implements for any repository service. @@ -29,12 +34,7 @@ interface BaseRepositoryService { type DtoBasedMutationMethods = "create" | "update" -export interface RepositoryService< - T = any, - TDTOs extends { [K in DtoBasedMutationMethods]?: any } = { - [K in DtoBasedMutationMethods]?: any - } -> extends BaseRepositoryService { +export interface RepositoryService extends BaseRepositoryService { find(options?: FindOptions, context?: Context): Promise findAndCount( @@ -42,34 +42,34 @@ export interface RepositoryService< context?: Context ): Promise<[T[], number]> - create(data: TDTOs["create"][], context?: Context): Promise + create(data: any[], context?: Context): Promise - update(data: TDTOs["update"][], context?: Context): Promise + update(data: { entity; update }[], context?: Context): Promise - delete(idsOrPKs: string[] | object[], context?: Context): Promise + delete( + idsOrPKs: FilterQuery & BaseFilterable>, + context?: Context + ): Promise /** * Soft delete entities and cascade to related entities if configured. * - * @param ids + * @param idsOrFilter * @param context * * @returns [T[], Record] the second value being the map of the entity names and ids that were soft deleted */ softDelete( - ids: string[], + idsOrFilter: string[] | InternalFilterQuery, context?: Context ): Promise<[T[], Record]> restore( - ids: string[], + idsOrFilter: string[] | InternalFilterQuery, context?: Context ): Promise<[T[], Record]> - upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - context?: Context - ): Promise + upsert(data: any[], context?: Context): Promise } export interface TreeRepositoryService diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index c69a3e90a1681..8780aa80ec829 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -10,6 +10,7 @@ import { Logger } from "../logger" export type Constructor = new (...args: any[]) => T export * from "../common/medusa-container" +export * from "./internal-module-service" export type LogLevel = | "query" diff --git a/packages/types/src/modules-sdk/internal-module-service.ts b/packages/types/src/modules-sdk/internal-module-service.ts new file mode 100644 index 0000000000000..10c1622511264 --- /dev/null +++ b/packages/types/src/modules-sdk/internal-module-service.ts @@ -0,0 +1,81 @@ +import { FindConfig } from "../common" +import { Context } from "../shared-context" +import { + BaseFilterable, + FilterQuery as InternalFilterQuery, + FilterQuery, +} from "../dal" + +export interface InternalModuleService< + TEntity extends {}, + TContainer extends object = object +> { + get __container__(): TContainer + + retrieve( + idOrObject: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + retrieve( + idOrObject: object, + config?: FindConfig, + sharedContext?: Context + ): Promise + + list( + filters?: FilterQuery | BaseFilterable>, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCount( + filters?: FilterQuery | BaseFilterable>, + config?: FindConfig, + sharedContext?: Context + ): Promise<[TEntity[], number]> + + create(data: any[], sharedContext?: Context): Promise + create(data: any, sharedContext?: Context): Promise + + update(data: any[], sharedContext?: Context): Promise + update(data: any, sharedContext?: Context): Promise + update( + selectorAndData: { + selector: FilterQuery | BaseFilterable> + data: any + }, + sharedContext?: Context + ): Promise + update( + selectorAndData: { + selector: FilterQuery | BaseFilterable> + data: any + }[], + sharedContext?: Context + ): Promise + + delete(idOrSelector: string, sharedContext?: Context): Promise + delete(idOrSelector: string[], sharedContext?: Context): Promise + delete(idOrSelector: object, sharedContext?: Context): Promise + delete(idOrSelector: object[], sharedContext?: Context): Promise + delete( + idOrSelector: { + selector: FilterQuery | BaseFilterable> + }, + sharedContext?: Context + ): Promise + + softDelete( + idsOrFilter: string[] | InternalFilterQuery, + sharedContext?: Context + ): Promise<[TEntity[], Record]> + + restore( + idsOrFilter: string[] | InternalFilterQuery, + sharedContext?: Context + ): Promise<[TEntity[], Record]> + + upsert(data: any[], sharedContext?: Context): Promise + upsert(data: any, sharedContext?: Context): Promise +} diff --git a/packages/types/src/modules-sdk/module-service.ts b/packages/types/src/modules-sdk/module-service.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/types/src/payment/service.ts b/packages/types/src/payment/service.ts index 94951f55455c0..ed5c12d60151f 100644 --- a/packages/types/src/payment/service.ts +++ b/packages/types/src/payment/service.ts @@ -54,11 +54,11 @@ export interface IPaymentModuleService extends IModuleService { sharedContext?: Context ): Promise - deletePaymentCollection( + deletePaymentCollections( paymentCollectionId: string[], sharedContext?: Context ): Promise - deletePaymentCollection( + deletePaymentCollections( paymentCollectionId: string, sharedContext?: Context ): Promise diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index bf433aaa890e5..0a88f48e95c6c 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -47,7 +47,7 @@ import { import { FindConfig } from "../common" import { ModuleJoinerConfig } from "../modules-sdk" import { Context } from "../shared-context" -import { RestoreReturn } from "../dal" +import { RestoreReturn, SoftDeleteReturn } from "../dal" export interface IPricingModuleService { /** @@ -1279,6 +1279,7 @@ export interface IPricingModuleService { * This method soft deletes money amounts by their IDs. * * @param {string[]} ids - The IDs of the money amounts to delete. + * @param {SoftDeleteReturn} config * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the money amounts are successfully deleted. * @@ -1295,7 +1296,11 @@ export interface IPricingModuleService { * ) * } */ - softDeleteMoneyAmounts(ids: string[], sharedContext?: Context): Promise + softDeleteMoneyAmounts( + ids: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> /** * This method restores soft deleted money amounts by their IDs. @@ -1305,10 +1310,10 @@ export interface IPricingModuleService { * Configurations determining which relations to restore along with each of the money amounts. You can pass to its `returnLinkableKeys` * property any of the money amount's relation attribute names, such as `price_set_money_amount`. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored, such as the ID of associated price set money amounts. - * The object's keys are the ID attribute names of the money amount entity's relations, such as `price_set_money_amount_id`, - * and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, + * @returns {Promise | void>} + * An object that includes the IDs of related records that were restored, such as the ID of associated price set money amounts. + * The object's keys are the ID attribute names of the money amount entity's relations, such as `price_set_money_amount_id`, + * and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, * such as the IDs of associated price set money amounts. * * @example @@ -1324,7 +1329,7 @@ export interface IPricingModuleService { * ) * } */ - restoreDeletedMoneyAmounts( + restoreMoneyAmounts( ids: string[], config?: RestoreReturn, sharedContext?: Context diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index 86c4593e16f23..9477fd31ef756 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -1493,24 +1493,24 @@ export interface IProductModuleService { /** * This method is used to update a product's variants. - * + * * @param {UpdateProductVariantDTO[]} data - The product variants to update. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated product variants's details. - * + * * @example * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * import { + * import { * UpdateProductVariantDTO * } from "@medusajs/product/dist/types/services/product-variant" - * + * * async function updateProductVariants (items: UpdateProductVariantDTO[]) { * const productModule = await initializeProductModule() - * + * * const productVariants = await productModule.updateVariants(items) - * + * * // do something with the product variants or return them * } */ @@ -1521,24 +1521,24 @@ export interface IProductModuleService { /** * This method is used to create variants for a product. - * + * * @param {CreateProductVariantDTO[]} data - The product variants to create. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created product variants' details. - * + * * @example * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductVariants (items: { * product_id: string, * title: string * }[]) { * const productModule = await initializeProductModule() - * + * * const productVariants = await productModule.createVariants(items) - * + * * // do something with the product variants or return them * } */ @@ -2513,16 +2513,16 @@ export interface IProductModuleService { ): Promise | void> /** - * This method is used to restore product varaints that were soft deleted. Product variants are soft deleted when they're not + * This method is used to restore product varaints that were soft deleted. Product variants are soft deleted when they're not * provided in a product's details passed to the {@link update} method. - * + * * @param {string[]} variantIds - The IDs of the variants to restore. - * @param {RestoreReturn} config - + * @param {RestoreReturn} config - * Configurations determining which relations to restore along with each of the product variants. You can pass to its `returnLinkableKeys` * property any of the product variant's relation attribute names. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product variant entity's relations + * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product variant entity's relations * and its value is an array of strings, each being the ID of the record associated with the product variant through this relation. * * If there are no related records that were restored, the promise resolved to `void`. diff --git a/packages/utils/src/common/__tests__/pluralize.spec.ts b/packages/utils/src/common/__tests__/pluralize.spec.ts new file mode 100644 index 0000000000000..8f3301bc05363 --- /dev/null +++ b/packages/utils/src/common/__tests__/pluralize.spec.ts @@ -0,0 +1,33 @@ +import { pluralize } from "../plurailze" + +describe("pluralize", function () { + it("should pluralize any words", function () { + const words = [ + "apple", + "box", + "day", + "country", + "baby", + "knife", + "hero", + "potato", + "address", + ] + + const expectedOutput = [ + "apples", + "boxes", + "days", + "countries", + "babies", + "knives", + "heroes", + "potatoes", + "addresses", + ] + + words.forEach((word, index) => { + expect(pluralize(word)).toBe(expectedOutput[index]) + }) + }) +}) diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 7dc7fd9831041..4f47fcc1ea4d0 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -26,6 +26,7 @@ export * from "./object-from-string-path" export * from "./object-to-string-path" export * from "./optional-numeric-serializer" export * from "./pick-value-from-object" +export * from "./plurailze" export * from "./promise-all" export * from "./remote-query-object-from-string" export * from "./remote-query-object-to-string" diff --git a/packages/utils/src/common/plurailze.ts b/packages/utils/src/common/plurailze.ts new file mode 100644 index 0000000000000..f35fb2d89bd88 --- /dev/null +++ b/packages/utils/src/common/plurailze.ts @@ -0,0 +1,27 @@ +/** + * Some library provide pluralize function with language specific rules. + * This is a simple implementation of pluralize function. + * @param word + */ +export function pluralize(word: string): string { + // Add basic rules for forming plurals + if ( + //word.endsWith("s") || + word.endsWith("sh") || + word.endsWith("ss") || + word.endsWith("ch") || + word.endsWith("x") || + word.endsWith("o") || + word.endsWith("z") + ) { + return word + "es" + } else if (word.endsWith("y") && !"aeiou".includes(word[word.length - 2])) { + return word.slice(0, -1) + "ies" + } else if (word.endsWith("es")) { + return word + } else if (word.endsWith("fe")) { + return word.slice(0, -2) + "ves" + } else { + return word + "s" + } +} diff --git a/packages/utils/src/dal/index.ts b/packages/utils/src/dal/index.ts index 5085c6d0a3754..2d87ea47cfaed 100644 --- a/packages/utils/src/dal/index.ts +++ b/packages/utils/src/dal/index.ts @@ -3,5 +3,4 @@ export * from "./mikro-orm/mikro-orm-repository" export * from "./mikro-orm/mikro-orm-soft-deletable-filter" export * from "./mikro-orm/utils" export * from "./repositories" -export * from "./repository" export * from "./utils" diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index ea4d185af60ff..745b0477954a4 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -1,6 +1,8 @@ import { + BaseFilterable, Context, DAL, + FilterQuery, FilterQuery as InternalFilterQuery, RepositoryService, RepositoryTransformOptions, @@ -17,11 +19,11 @@ import { EntityName, FilterQuery as MikroFilterQuery, } from "@mikro-orm/core/typings" -import { MedusaError, isString } from "../../common" +import { isString } from "../../common" import { + buildQuery, InjectTransactionManager, MedusaContext, - buildQuery, } from "../../modules-sdk" import { getSoftDeletedCascadedEntitiesIdsMappedBy, @@ -42,10 +44,10 @@ export class MikroOrmBase { : this.manager_) as unknown as TManager } - getActiveManager( - @MedusaContext() - { transactionManager, manager }: Context = {} - ): TManager { + getActiveManager({ + transactionManager, + manager, + }: Context = {}): TManager { return (transactionManager ?? manager ?? this.manager_) as TManager } @@ -77,9 +79,10 @@ export class MikroOrmBase { * related ones. */ -export class MikroOrmBaseRepository< - T extends object = object -> extends MikroOrmBase { +export class MikroOrmBaseRepository + extends MikroOrmBase + implements RepositoryService +{ constructor(...args: any[]) { // @ts-ignore super(...arguments) @@ -89,11 +92,14 @@ export class MikroOrmBaseRepository< throw new Error("Method not implemented.") } - update(data: unknown[], context?: Context): Promise { + update(data: { entity; update }[], context?: Context): Promise { throw new Error("Method not implemented.") } - delete(ids: string[] | object[], context?: Context): Promise { + delete( + idsOrPKs: FilterQuery & BaseFilterable>, + context?: Context + ): Promise { throw new Error("Method not implemented.") } @@ -115,10 +121,10 @@ export class MikroOrmBaseRepository< @InjectTransactionManager() async softDelete( idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() - { transactionManager: manager }: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise<[T[], Record]> { const isArray = Array.isArray(idsOrFilter) + // TODO handle composite keys const filter = isArray || isString(idsOrFilter) ? { @@ -128,9 +134,10 @@ export class MikroOrmBaseRepository< } : idsOrFilter - const entities = await this.find({ where: filter as any }) + const entities = await this.find({ where: filter as any }, sharedContext) const date = new Date() + const manager = this.getActiveManager(sharedContext) await mikroOrmUpdateDeletedAtRecursively( manager, entities as any[], @@ -147,9 +154,9 @@ export class MikroOrmBaseRepository< @InjectTransactionManager() async restore( idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() - { transactionManager: manager }: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise<[T[], Record]> { + // TODO handle composite keys const isArray = Array.isArray(idsOrFilter) const filter = isArray || isString(idsOrFilter) @@ -164,8 +171,9 @@ export class MikroOrmBaseRepository< withDeleted: true, }) - const entities = await this.find(query) + const entities = await this.find(query, sharedContext) + const manager = this.getActiveManager(sharedContext) await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null) const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({ @@ -228,18 +236,12 @@ export class MikroOrmBaseTreeRepository< } } -type DtoBasedMutationMethods = "create" | "update" - -export function mikroOrmBaseRepositoryFactory< - T extends object = object, - TDTOs extends { [K in DtoBasedMutationMethods]?: any } = { - [K in DtoBasedMutationMethods]?: any - } ->(entity: EntityClass | EntitySchema) { - class MikroOrmAbstractBaseRepository_ - extends MikroOrmBaseRepository - implements RepositoryService - { +export function mikroOrmBaseRepositoryFactory( + entity: any +): { + new ({ manager }: { manager: any }): MikroOrmBaseRepository +} { + class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository { // @ts-ignore constructor(...args: any[]) { // @ts-ignore @@ -257,7 +259,7 @@ export function mikroOrmBaseRepositoryFactory< ) } - async create(data: TDTOs["create"][], context?: Context): Promise { + async create(data: any[], context?: Context): Promise { const manager = this.getActiveManager(context) const entities = data.map((data_) => { @@ -272,76 +274,13 @@ export function mikroOrmBaseRepositoryFactory< return entities } - async update(data: TDTOs["update"][], context?: Context): Promise { - // TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts + async update(data: { entity; update }[], context?: Context): Promise { const manager = this.getActiveManager(context) - - const primaryKeys = - MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) - - let primaryKeysCriteria: { [key: string]: any }[] = [] - if (primaryKeys.length === 1) { - primaryKeysCriteria.push({ - [primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]), - }) - } else { - primaryKeysCriteria = data.map((d) => ({ - $and: primaryKeys.map((key) => ({ [key]: d[key] })), - })) - } - - const allEntities = await Promise.all( - primaryKeysCriteria.map( - async (criteria) => - await this.find({ where: criteria } as DAL.FindOptions, context) - ) - ) - - const existingEntities = allEntities.flat() - - const existingEntitiesMap = new Map() - existingEntities.forEach((entity) => { - if (entity) { - const key = - MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue( - primaryKeys, - entity - ) - existingEntitiesMap.set(key, entity) - } - }) - - const missingEntities = data.filter((data_) => { - const key = - MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue( - primaryKeys, - data_ - ) - return !existingEntitiesMap.has(key) - }) - - if (missingEntities.length) { - const entityName = (entity as EntityClass).name ?? entity - const missingEntitiesKeys = data.map((data_) => - primaryKeys.map((key) => data_[key]).join(", ") - ) - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `${entityName} with ${primaryKeys.join( - ", " - )} "${missingEntitiesKeys.join(", ")}" not found` - ) - } - const entities = data.map((data_) => { - const key = - MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue( - primaryKeys, - data_ - ) - const existingEntity = existingEntitiesMap.get(key)! - - return manager.assign(existingEntity, data_ as RequiredEntityData) + return manager.assign( + data_.entity, + data_.update as RequiredEntityData + ) }) manager.persist(entities) @@ -350,40 +289,11 @@ export function mikroOrmBaseRepositoryFactory< } async delete( - primaryKeyValues: string[] | object[], + filters: FilterQuery & BaseFilterable>, context?: Context ): Promise { const manager = this.getActiveManager(context) - - const primaryKeys = - MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) - - let deletionCriteria - if (primaryKeys.length > 1) { - deletionCriteria = { - $or: primaryKeyValues.map((compositeKeyValue) => { - const keys = Object.keys(compositeKeyValue) - if (!primaryKeys.every((k) => keys.includes(k))) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Composite key must contain all primary key fields: ${primaryKeys.join( - ", " - )}. Found: ${keys}` - ) - } - - const criteria: { [key: string]: any } = {} - for (const key of primaryKeys) { - criteria[key] = compositeKeyValue[key] - } - return criteria - }), - } - } else { - deletionCriteria = { [primaryKeys[0]]: { $in: primaryKeyValues } } - } - - await manager.nativeDelete(entity as EntityName, deletionCriteria) + await manager.nativeDelete(entity as EntityName, filters as any) } async find(options?: DAL.FindOptions, context?: Context): Promise { @@ -423,11 +333,7 @@ export function mikroOrmBaseRepositoryFactory< ) } - async upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - context: Context = {} - ): Promise { - // TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts + async upsert(data: any[], context: Context = {}): Promise { const manager = this.getActiveManager(context) const primaryKeys = @@ -435,21 +341,34 @@ export function mikroOrmBaseRepositoryFactory< let primaryKeysCriteria: { [key: string]: any }[] = [] if (primaryKeys.length === 1) { - primaryKeysCriteria.push({ - [primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]), - }) + const primaryKeyValues = data + .map((d) => d[primaryKeys[0]]) + .filter(Boolean) + + if (primaryKeyValues.length) { + primaryKeysCriteria.push({ + [primaryKeys[0]]: primaryKeyValues, + }) + } } else { primaryKeysCriteria = data.map((d) => ({ $and: primaryKeys.map((key) => ({ [key]: d[key] })), })) } - const allEntities = await Promise.all( - primaryKeysCriteria.map( - async (criteria) => - await this.find({ where: criteria } as DAL.FindOptions, context) + let allEntities: T[][] = [] + + if (primaryKeysCriteria.length) { + allEntities = await Promise.all( + primaryKeysCriteria.map( + async (criteria) => + await this.find( + { where: criteria } as DAL.FindOptions, + context + ) + ) ) - ) + } const existingEntities = allEntities.flat() @@ -482,7 +401,7 @@ export function mikroOrmBaseRepositoryFactory< const updatedType = manager.assign(existingEntity, data_) updatedEntities.push(updatedType) } else { - const newEntity = manager.create(entity, data_) + const newEntity = manager.create(entity, data_) createdEntities.push(newEntity) } }) @@ -497,9 +416,12 @@ export function mikroOrmBaseRepositoryFactory< upsertedEntities.push(...updatedEntities) } + // TODO return the all, created, updated entities return upsertedEntities } } - return MikroOrmAbstractBaseRepository_ + return MikroOrmAbstractBaseRepository_ as unknown as { + new ({ manager }: { manager: any }): MikroOrmBaseRepository + } } diff --git a/packages/utils/src/dal/mikro-orm/utils.ts b/packages/utils/src/dal/mikro-orm/utils.ts index fc5d1f55d78a3..43a5a5efa218b 100644 --- a/packages/utils/src/dal/mikro-orm/utils.ts +++ b/packages/utils/src/dal/mikro-orm/utils.ts @@ -1,3 +1,5 @@ +import { buildQuery } from "../../modules-sdk" + export const mikroOrmUpdateDeletedAtRecursively = async < T extends object = any >( @@ -27,12 +29,35 @@ export const mikroOrmUpdateDeletedAtRecursively = async < continue } + const retrieveEntity = async () => { + const query = buildQuery( + { + id: entity.id, + }, + { + relations: [relation.name], + withDeleted: true, + } + ) + return await manager.findOne( + entity.constructor.name, + query.where, + query.options + ) + } + + if (!entityRelation) { + // Fixes the case of many to many through pivot table + entityRelation = await retrieveEntity() + } + const isCollection = "toArray" in entityRelation let relationEntities: any[] = [] if (isCollection) { if (!entityRelation.isInitialized()) { - entityRelation = await entityRelation.init({ populate: true }) + entityRelation = await retrieveEntity() + entityRelation = entityRelation[relation.name] } relationEntities = entityRelation.getItems() } else { @@ -53,6 +78,10 @@ export const mikroOrmSerializer = async ( ): Promise => { options ??= {} const { serialize } = await import("@mikro-orm/core") - const result = serialize(data, options) + const result = serialize(data, { + forceObject: true, + populate: true, + ...options, + }) return result as unknown as Promise } diff --git a/packages/utils/src/dal/repository.ts b/packages/utils/src/dal/repository.ts deleted file mode 100644 index f2f3da05a30b1..0000000000000 --- a/packages/utils/src/dal/repository.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types" -import { MedusaContext } from "../modules-sdk" -import { transactionWrapper } from "./utils" - -class AbstractBase { - protected readonly manager_: any - - protected constructor({ manager }) { - this.manager_ = manager - } - - getActiveManager( - @MedusaContext() - { transactionManager, manager }: Context = {} - ): TManager { - return (transactionManager ?? manager ?? this.manager_) as TManager - } - - async transaction( - task: (transactionManager: TManager) => Promise, - { - transaction, - isolationLevel, - enableNestedTransactions = false, - }: { - isolationLevel?: string - enableNestedTransactions?: boolean - transaction?: TManager - } = {} - ): Promise { - // @ts-ignore - return await transactionWrapper.apply(this, arguments) - } -} - -export abstract class AbstractBaseRepository - extends AbstractBase - implements DAL.RepositoryService -{ - abstract find(options?: DAL.FindOptions, context?: Context) - - abstract findAndCount( - options?: DAL.FindOptions, - context?: Context - ): Promise<[T[], number]> - - abstract create(data: unknown[], context?: Context): Promise - - abstract update(data: unknown[], context?: Context): Promise - - abstract delete(ids: string[], context?: Context): Promise - - abstract upsert(data: unknown[], context?: Context): Promise - - abstract softDelete( - ids: string[], - context?: Context - ): Promise<[T[], Record]> - - abstract restore( - ids: string[], - context?: Context - ): Promise<[T[], Record]> - - abstract getFreshManager(): TManager - - abstract serialize( - data: any, - options?: any - ): Promise -} - -export abstract class AbstractTreeRepositoryBase - extends AbstractBase - implements DAL.TreeRepositoryService -{ - protected constructor({ manager }) { - // @ts-ignore - super(...arguments) - } - - abstract find( - options?: DAL.FindOptions, - transformOptions?: RepositoryTransformOptions, - context?: Context - ) - - abstract findAndCount( - options?: DAL.FindOptions, - transformOptions?: RepositoryTransformOptions, - context?: Context - ): Promise<[T[], number]> - - abstract create(data: unknown, context?: Context): Promise - - abstract delete(id: string, context?: Context): Promise - - abstract getFreshManager(): TManager - - abstract serialize( - data: any, - options?: any - ): Promise -} diff --git a/packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts b/packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts new file mode 100644 index 0000000000000..35935e84ed308 --- /dev/null +++ b/packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts @@ -0,0 +1,201 @@ +import { abstractModuleServiceFactory } from "../abstract-module-service-factory" + +const baseRepoMock = { + serialize: jest.fn().mockImplementation((item) => item), + transaction: (task) => task("transactionManager"), + getFreshManager: jest.fn().mockReturnThis(), +} + +const defaultContext = { __type: "MedusaContext", manager: baseRepoMock } +const defaultTransactionContext = { + __type: "MedusaContext", + transactionManager: "transactionManager", +} + +describe("Abstract Module Service Factory", () => { + const containerMock = { + baseRepository: baseRepoMock, + mainModelMockRepository: baseRepoMock, + otherModelMock1Repository: baseRepoMock, + otherModelMock2Repository: baseRepoMock, + mainModelMockService: { + retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), + list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + delete: jest.fn().mockResolvedValue(undefined), + softDelete: jest.fn().mockResolvedValue([[], {}]), + restore: jest.fn().mockResolvedValue([[], {}]), + }, + otherModelMock1Service: { + retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), + list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + delete: jest.fn().mockResolvedValue(undefined), + softDelete: jest.fn().mockResolvedValue([[], {}]), + restore: jest.fn().mockResolvedValue([[], {}]), + }, + otherModelMock2Service: { + retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), + list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + delete: jest.fn().mockResolvedValue(undefined), + softDelete: jest.fn().mockResolvedValue([[], {}]), + restore: jest.fn().mockResolvedValue([[], {}]), + }, + } + + const mainModelMock = class MainModelMock {} + const otherModelMock1 = class OtherModelMock1 {} + const otherModelMock2 = class OtherModelMock2 {} + + const abstractModuleService = abstractModuleServiceFactory< + any, + any, + { + OtherModelMock1: { + dto: any + singular: "OtherModelMock1" + plural: "OtherModelMock1s" + } + OtherModelMock2: { + dto: any + singular: "OtherModelMock2" + plural: "OtherModelMock2s" + } + } + >( + mainModelMock, + [ + { + model: otherModelMock1, + plural: "otherModelMock1s", + singular: "otherModelMock1", + }, + { + model: otherModelMock2, + plural: "otherModelMock2s", + singular: "otherModelMock2", + }, + ] + // Add more parameters as needed + ) + + describe("Main Model Methods", () => { + let instance + + beforeEach(() => { + jest.clearAllMocks() + instance = new abstractModuleService(containerMock) + }) + + test("should have retrieve method", async () => { + const result = await instance.retrieve("1") + expect(result).toEqual({ id: "1", name: "Item" }) + expect(containerMock.mainModelMockService.retrieve).toHaveBeenCalledWith( + "1", + undefined, + defaultContext + ) + }) + + test("should have list method", async () => { + const result = await instance.list() + expect(result).toEqual([{ id: "1", name: "Item" }]) + expect(containerMock.mainModelMockService.list).toHaveBeenCalledWith( + {}, + {}, + defaultContext + ) + }) + + test("should have delete method", async () => { + await instance.delete("1") + expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have softDelete method", async () => { + const result = await instance.softDelete("1") + expect(result).toEqual(undefined) + expect( + containerMock.mainModelMockService.softDelete + ).toHaveBeenCalledWith(["1"], defaultTransactionContext) + }) + + test("should have restore method", async () => { + const result = await instance.restore("1") + expect(result).toEqual(undefined) + expect(containerMock.mainModelMockService.restore).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have delete method with selector", async () => { + await instance.delete({ selector: { id: "1" } }) + expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith( + [{ selector: { id: "1" } }], + defaultTransactionContext + ) + }) + }) + + describe("Other Models Methods", () => { + let instance + + beforeEach(() => { + jest.clearAllMocks() + instance = new abstractModuleService(containerMock) + }) + + test("should have retrieve method for other models", async () => { + const result = await instance.retrieveOtherModelMock1("1") + expect(result).toEqual({ id: "1", name: "Item" }) + expect( + containerMock.otherModelMock1Service.retrieve + ).toHaveBeenCalledWith("1", undefined, defaultContext) + }) + + test("should have list method for other models", async () => { + const result = await instance.listOtherModelMock1s() + expect(result).toEqual([{ id: "1", name: "Item" }]) + expect(containerMock.otherModelMock1Service.list).toHaveBeenCalledWith( + {}, + {}, + defaultContext + ) + }) + + test("should have delete method for other models", async () => { + await instance.deleteOtherModelMock1s("1") + expect(containerMock.otherModelMock1Service.delete).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have softDelete method for other models", async () => { + const result = await instance.softDeleteOtherModelMock1s("1") + expect(result).toEqual(undefined) + expect( + containerMock.otherModelMock1Service.softDelete + ).toHaveBeenCalledWith(["1"], defaultTransactionContext) + }) + + test("should have restore method for other models", async () => { + const result = await instance.restoreOtherModelMock1s("1") + expect(result).toEqual(undefined) + expect(containerMock.otherModelMock1Service.restore).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have delete method for other models with selector", async () => { + await instance.deleteOtherModelMock1s({ selector: { id: "1" } }) + expect(containerMock.otherModelMock1Service.delete).toHaveBeenCalledWith( + [{ selector: { id: "1" } }], + defaultTransactionContext + ) + }) + }) +}) diff --git a/packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts b/packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts new file mode 100644 index 0000000000000..2e4b9db5c409b --- /dev/null +++ b/packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts @@ -0,0 +1,240 @@ +import { internalModuleServiceFactory } from "../internal-module-service-factory" +import { lowerCaseFirst } from "../../common" + +const defaultContext = { __type: "MedusaContext" } + +class Model {} +describe("Internal Module Service Factory", () => { + const modelRepositoryName = `${lowerCaseFirst(Model.name)}Repository` + + const containerMock = { + [modelRepositoryName]: { + transaction: (task) => task(), + getFreshManager: jest.fn().mockReturnThis(), + find: jest.fn(), + findAndCount: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + softDelete: jest.fn(), + restore: jest.fn(), + upsert: jest.fn(), + }, + [`composite${Model.name}Repository`]: { + transaction: (task) => task(), + getFreshManager: jest.fn().mockReturnThis(), + find: jest.fn(), + findAndCount: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + softDelete: jest.fn(), + restore: jest.fn(), + upsert: jest.fn(), + }, + } + + const internalModuleService = internalModuleServiceFactory(Model) + + describe("Internal Module Service Methods", () => { + let instance + + beforeEach(() => { + jest.clearAllMocks() + instance = new internalModuleService(containerMock) + }) + + test("should throw model id undefined error on retrieve if id is not defined", async () => { + const err = await instance.retrieve().catch((e) => e) + expect(err.message).toBe("model - id must be defined") + }) + + test("should throw an error on retrieve if composite key values are not defined", async () => { + class CompositeModel { + id: string + name: string + + static meta = { primaryKeys: ["id", "name"] } + } + + const compositeInternalModuleService = + internalModuleServiceFactory(CompositeModel) + + const instance = new compositeInternalModuleService(containerMock) + + const err = await instance.retrieve().catch((e) => e) + expect(err.message).toBe("compositeModel - id, name must be defined") + }) + + test("should throw NOT_FOUND error on retrieve if entity not found", async () => { + containerMock[modelRepositoryName].find.mockResolvedValueOnce([]) + + const err = await instance.retrieve("1").catch((e) => e) + expect(err.message).toBe("Model with id: 1 was not found") + }) + + test("should retrieve entity successfully", async () => { + const entity = { id: "1", name: "Item" } + containerMock[modelRepositoryName].find.mockResolvedValueOnce([entity]) + + const result = await instance.retrieve("1") + expect(result).toEqual(entity) + }) + + test("should retrieve entity successfully with composite key", async () => { + class CompositeModel { + id: string + name: string + + static meta = { primaryKeys: ["id", "name"] } + } + + const compositeInternalModuleService = + internalModuleServiceFactory(CompositeModel) + + const instance = new compositeInternalModuleService(containerMock) + + const entity = { id: "1", name: "Item" } + containerMock[ + `${lowerCaseFirst(CompositeModel.name)}Repository` + ].find.mockResolvedValueOnce([entity]) + + const result = await instance.retrieve({ id: "1", name: "Item" }) + expect(result).toEqual(entity) + }) + + test("should list entities successfully", async () => { + const entities = [ + { id: "1", name: "Item" }, + { id: "2", name: "Item2" }, + ] + containerMock[modelRepositoryName].find.mockResolvedValueOnce(entities) + + const result = await instance.list() + expect(result).toEqual(entities) + }) + + test("should list and count entities successfully", async () => { + const entities = [ + { id: "1", name: "Item" }, + { id: "2", name: "Item2" }, + ] + const count = entities.length + containerMock[modelRepositoryName].findAndCount.mockResolvedValueOnce([ + entities, + count, + ]) + + const result = await instance.listAndCount() + expect(result).toEqual([entities, count]) + }) + + test("should create entity successfully", async () => { + const entity = { id: "1", name: "Item" } + + containerMock[modelRepositoryName].find.mockReturnValue([entity]) + + containerMock[modelRepositoryName].create.mockImplementation( + async (entity) => entity + ) + + const result = await instance.create(entity) + expect(result).toEqual(entity) + }) + + test("should create entities successfully", async () => { + const entities = [ + { id: "1", name: "Item" }, + { id: "2", name: "Item2" }, + ] + + containerMock[modelRepositoryName].find.mockResolvedValueOnce([entities]) + + containerMock[modelRepositoryName].create.mockResolvedValueOnce(entities) + + const result = await instance.create(entities) + expect(result).toEqual(entities) + }) + + test("should update entity successfully", async () => { + const updateData = { id: "1", name: "UpdatedItem" } + + containerMock[modelRepositoryName].find.mockResolvedValueOnce([ + updateData, + ]) + + containerMock[modelRepositoryName].update.mockResolvedValueOnce([ + updateData, + ]) + + const result = await instance.update(updateData) + expect(result).toEqual([updateData]) + }) + + test("should update entities successfully", async () => { + const updateData = { id: "1", name: "UpdatedItem" } + const entitiesToUpdate = [{ id: "1", name: "Item" }] + + containerMock[modelRepositoryName].find.mockResolvedValueOnce( + entitiesToUpdate + ) + + containerMock[modelRepositoryName].update.mockResolvedValueOnce([ + { entity: entitiesToUpdate[0], update: updateData }, + ]) + + const result = await instance.update({ selector: {}, data: updateData }) + expect(result).toEqual([ + { entity: entitiesToUpdate[0], update: updateData }, + ]) + }) + + test("should delete entity successfully", async () => { + await instance.delete("1") + expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith( + { + $or: [ + { + id: "1", + }, + ], + }, + defaultContext + ) + }) + + test("should delete entities successfully", async () => { + const entitiesToDelete = [{ id: "1", name: "Item" }] + containerMock[modelRepositoryName].find.mockResolvedValueOnce( + entitiesToDelete + ) + + await instance.delete({ selector: {} }) + expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith( + { + $or: [ + { + id: "1", + }, + ], + }, + defaultContext + ) + }) + + test("should soft delete entity successfully", async () => { + await instance.softDelete("1") + expect( + containerMock[modelRepositoryName].softDelete + ).toHaveBeenCalledWith("1", defaultContext) + }) + + test("should restore entity successfully", async () => { + await instance.restore("1") + expect(containerMock[modelRepositoryName].restore).toHaveBeenCalledWith( + "1", + defaultContext + ) + }) + }) +}) diff --git a/packages/utils/src/modules-sdk/abstract-module-service-factory.ts b/packages/utils/src/modules-sdk/abstract-module-service-factory.ts new file mode 100644 index 0000000000000..e6e23257dff8d --- /dev/null +++ b/packages/utils/src/modules-sdk/abstract-module-service-factory.ts @@ -0,0 +1,527 @@ +/** + * Utility factory and interfaces for module service public facing API + */ +import { + Constructor, + Context, + FindConfig, + IEventBusModuleService, + Pluralize, + RepositoryService, + RestoreReturn, + SoftDeleteReturn, +} from "@medusajs/types" +import { + isString, + kebabCase, + lowerCaseFirst, + mapObjectTo, + MapToConfig, + pluralize, + upperCaseFirst, +} from "../common" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "./decorators" + +type BaseMethods = + | "retrieve" + | "list" + | "listAndCount" + | "delete" + | "softDelete" + | "restore" + +const readMethods = ["retrieve", "list", "listAndCount"] as BaseMethods[] +const writeMethods = ["delete", "softDelete", "restore"] as BaseMethods[] + +const methods: BaseMethods[] = [...readMethods, ...writeMethods] + +type ModelsConfigTemplate = { + [ModelName: string]: { singular?: string; plural?: string; dto: object } +} + +type ExtractSingularName< + T extends Record, + K = keyof T +> = T[K] extends { singular?: string } ? T[K]["singular"] : K + +type ExtractPluralName, K = keyof T> = T[K] extends { + plural?: string +} + ? T[K]["plural"] + : Pluralize + +type ModelConfiguration = + | Constructor + | { singular?: string; plural?: string; model: Constructor } + +export interface AbstractModuleServiceBase { + get __container__(): TContainer + + retrieve( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + list( + filters?: any, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCount( + filters?: any, + config?: FindConfig, + sharedContext?: Context + ): Promise<[TMainModelDTO[], number]> + + delete( + primaryKeyValues: string | object | string[] | object[], + sharedContext?: Context + ): Promise + + softDelete( + primaryKeyValues: string | object | string[] | object[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restore( + primaryKeyValues: string | object | string[] | object[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> +} + +/** + * Multiple issues on typescript around mapped types function are open, so + * when overriding a method from the base class that is mapped dynamically from the + * other models, we will have to ignore the error (2425) + * + * see: https://github.com/microsoft/TypeScript/issues/48125 + */ +export type AbstractModuleService< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO extends ModelsConfigTemplate +> = AbstractModuleServiceBase & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `retrieve${ExtractSingularName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: ( + id: string, + config?: FindConfig, + sharedContext?: Context + ) => Promise +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `list${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: ( + filters?: any, + config?: FindConfig, + sharedContext?: Context + ) => Promise +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `listAndCount${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + (filters?: any, config?: FindConfig, sharedContext?: Context): Promise< + [TOtherModelNamesAndAssociatedDTO[K & string]["dto"][], number] + > + } +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `delete${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + ( + primaryKeyValues: string | object | string[] | object[], + sharedContext?: Context + ): Promise + } +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `softDelete${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + ( + primaryKeyValues: string | object | string[] | object[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + } +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `restore${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + ( + primaryKeyValues: string | object | string[] | object[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + } +} + +/** + * Factory function for creating an abstract module service + * + * @example + * + * const otherModels = new Set([ + * Currency, + * MoneyAmount, + * PriceList, + * PriceListRule, + * PriceListRuleValue, + * PriceRule, + * PriceSetMoneyAmount, + * PriceSetMoneyAmountRules, + * PriceSetRuleType, + * RuleType, + * ]) + * + * const AbstractModuleService = ModulesSdkUtils.abstractModuleServiceFactory< + * InjectedDependencies, + * PricingTypes.PriceSetDTO, + * // The configuration of each entity also accept singular/plural properties, if not provided then it is using english pluralization + * { + * Currency: { dto: PricingTypes.CurrencyDTO } + * MoneyAmount: { dto: PricingTypes.MoneyAmountDTO } + * PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO } + * PriceSetMoneyAmountRules: { + * dto: PricingTypes.PriceSetMoneyAmountRulesDTO + * } + * PriceRule: { dto: PricingTypes.PriceRuleDTO } + * RuleType: { dto: PricingTypes.RuleTypeDTO } + * PriceList: { dto: PricingTypes.PriceListDTO } + * PriceListRule: { dto: PricingTypes.PriceListRuleDTO } + * } + * >(PriceSet, [...otherModels], entityNameToLinkableKeysMap) + * + * @param mainModel + * @param otherModels + * @param entityNameToLinkableKeysMap + */ +export function abstractModuleServiceFactory< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO extends ModelsConfigTemplate +>( + mainModel: Constructor, + otherModels: ModelConfiguration[], + entityNameToLinkableKeysMap: MapToConfig = {} +): { + new (container: TContainer): AbstractModuleService< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO + > +} { + const buildMethodNamesFromModel = ( + model: ModelConfiguration, + suffixed: boolean = true + ): Record => { + return methods.reduce((acc, method) => { + let modelName: string = "" + + if (method === "retrieve") { + modelName = + "singular" in model && model.singular + ? model.singular + : (model as Constructor).name + } else { + modelName = + "plural" in model && model.plural + ? model.plural + : pluralize((model as Constructor).name) + } + + const methodName = suffixed + ? `${method}${upperCaseFirst(modelName)}` + : method + + return { ...acc, [method]: methodName } + }, {}) + } + + const buildAndAssignMethodImpl = function ( + klassPrototype: any, + method: string, + methodName: string, + model: Constructor + ): void { + const serviceRegistrationName = `${lowerCaseFirst(model.name)}Service` + + const applyMethod = function (impl: Function, contextIndex) { + klassPrototype[methodName] = impl + + const descriptorMockRef = { + value: klassPrototype[methodName], + } + + MedusaContext()(klassPrototype, methodName, contextIndex) + + const ManagerDecorator = readMethods.includes(method as BaseMethods) + ? InjectManager + : InjectTransactionManager + + ManagerDecorator("baseRepository_")( + klassPrototype, + methodName, + descriptorMockRef + ) + + klassPrototype[methodName] = descriptorMockRef.value + } + + let methodImplementation: any = function () { + void 0 + } + + switch (method) { + case "retrieve": + methodImplementation = async function ( + this: AbstractModuleService_, + id: string, + config?: FindConfig, + sharedContext: Context = {} + ): Promise { + const entities = await this.__container__[ + serviceRegistrationName + ].retrieve(id, config, sharedContext) + + return await this.baseRepository_.serialize(entities, { + populate: true, + }) + } + + applyMethod(methodImplementation, 2) + + break + case "list": + methodImplementation = async function ( + this: AbstractModuleService_, + filters = {}, + config: FindConfig = {}, + sharedContext: Context = {} + ): Promise { + const entities = await this.__container__[ + serviceRegistrationName + ].list(filters, config, sharedContext) + + return await this.baseRepository_.serialize(entities, { + populate: true, + }) + } + + applyMethod(methodImplementation, 2) + + break + case "listAndCount": + methodImplementation = async function ( + this: AbstractModuleService_, + filters = {}, + config: FindConfig = {}, + sharedContext: Context = {} + ): Promise { + const [entities, count] = await this.__container__[ + serviceRegistrationName + ].listAndCount(filters, config, sharedContext) + + return [ + await this.baseRepository_.serialize(entities, { + populate: true, + }), + count, + ] + } + + applyMethod(methodImplementation, 2) + + break + case "delete": + methodImplementation = async function ( + this: AbstractModuleService_, + primaryKeyValues: string | object | string[] | object[], + sharedContext: Context = {} + ): Promise { + const primaryKeyValues_ = Array.isArray(primaryKeyValues) + ? primaryKeyValues + : [primaryKeyValues] + await this.__container__[serviceRegistrationName].delete( + primaryKeyValues_, + sharedContext + ) + + await this.eventBusModuleService_?.emit( + primaryKeyValues_.map((primaryKeyValue) => ({ + eventName: `${kebabCase(model.name)}.deleted`, + data: isString(primaryKeyValue) + ? { id: primaryKeyValue } + : primaryKeyValue, + })) + ) + } + + applyMethod(methodImplementation, 1) + + break + case "softDelete": + methodImplementation = async function ( + this: AbstractModuleService_, + primaryKeyValues: string | object | string[] | object[], + config: SoftDeleteReturn = {}, + sharedContext: Context = {} + ): Promise | void> { + const primaryKeyValues_ = Array.isArray(primaryKeyValues) + ? primaryKeyValues + : [primaryKeyValues] + + const [entities, cascadedEntitiesMap] = await this.__container__[ + serviceRegistrationName + ].softDelete(primaryKeyValues_, sharedContext) + + const softDeletedEntities = await this.baseRepository_.serialize( + entities, + { + populate: true, + } + ) + + await this.eventBusModuleService_?.emit( + softDeletedEntities.map(({ id }) => ({ + eventName: `${kebabCase(model.name)}.deleted`, + data: { id }, + })) + ) + + let mappedCascadedEntitiesMap + if (config.returnLinkableKeys) { + // Map internal table/column names to their respective external linkable keys + // eg: product.id = product_id, variant.id = variant_id + mappedCascadedEntitiesMap = mapObjectTo( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + applyMethod(methodImplementation, 2) + + break + case "restore": + methodImplementation = async function ( + this: AbstractModuleService_, + primaryKeyValues: string | object | string[] | object[], + config: RestoreReturn = {}, + sharedContext: Context = {} + ): Promise | void> { + const primaryKeyValues_ = Array.isArray(primaryKeyValues) + ? primaryKeyValues + : [primaryKeyValues] + + const [_, cascadedEntitiesMap] = await this.__container__[ + serviceRegistrationName + ].restore(primaryKeyValues_, sharedContext) + + let mappedCascadedEntitiesMap + if (config.returnLinkableKeys) { + // Map internal table/column names to their respective external linkable keys + // eg: product.id = product_id, variant.id = variant_id + mappedCascadedEntitiesMap = mapObjectTo( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + applyMethod(methodImplementation, 2) + + break + } + } + + class AbstractModuleService_ { + readonly __container__: Record + readonly baseRepository_: RepositoryService + readonly eventBusModuleService_: IEventBusModuleService; + + [key: string]: any + + constructor(container: Record) { + this.__container__ = container + this.baseRepository_ = container.baseRepository + + try { + this.eventBusModuleService_ = container.eventBusModuleService + } catch { + /* ignore */ + } + } + } + + const mainModelMethods = buildMethodNamesFromModel(mainModel, false) + + /** + * Build the main retrieve/list/listAndCount/delete/softDelete/restore methods for the main model + */ + + for (let [method, methodName] of Object.entries(mainModelMethods)) { + buildAndAssignMethodImpl( + AbstractModuleService_.prototype, + method, + methodName, + mainModel + ) + } + + /** + * Build the retrieve/list/listAndCount/delete/softDelete/restore methods for all the other models + */ + + const otherModelsMethods: [ModelConfiguration, Record][] = + otherModels.map((model) => [model, buildMethodNamesFromModel(model)]) + + for (let [model, modelsMethods] of otherModelsMethods) { + Object.entries(modelsMethods).forEach(([method, methodName]) => { + model = "model" in model ? model.model : model + buildAndAssignMethodImpl( + AbstractModuleService_.prototype, + method, + methodName, + model + ) + }) + } + + return AbstractModuleService_ as unknown as new ( + container: TContainer + ) => AbstractModuleService< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO + > +} diff --git a/packages/utils/src/modules-sdk/abstract-service-factory.ts b/packages/utils/src/modules-sdk/abstract-service-factory.ts deleted file mode 100644 index 64a6531bb3c8c..0000000000000 --- a/packages/utils/src/modules-sdk/abstract-service-factory.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { - Context, - 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, - shouldForceTransaction, - upperCaseFirst, -} from "../common" -import { MedusaContext } from "../modules-sdk" -import { buildQuery } from "./build-query" -import { InjectManager, InjectTransactionManager } from "./decorators" - -/** - * Utility factory and interfaces for internal module services - */ - -type FilterableMethods = "list" | "listAndCount" -type Methods = "create" | "update" - -export interface AbstractService< - TEntity extends {}, - TContainer extends object = object, - TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any }, - TFilters extends { [K in FilterableMethods]?: any } = { - [K in FilterableMethods]?: any - } -> { - get __container__(): TContainer - - retrieve( - id: string, - config?: FindConfig, - sharedContext?: Context - ): Promise - list( - filters?: TFilters["list"], - config?: FindConfig, - sharedContext?: Context - ): Promise - listAndCount( - filters?: TFilters["listAndCount"], - config?: FindConfig, - sharedContext?: Context - ): Promise<[TEntity[], number]> - create(data: TDTOs["create"][], sharedContext?: Context): Promise - update(data: TDTOs["update"][], sharedContext?: Context): Promise - delete( - primaryKeyValues: string[] | object[], - sharedContext?: Context - ): Promise - softDelete( - idsOrFilter: string[] | InternalFilterQuery, - sharedContext?: Context - ): Promise<[TEntity[], Record]> - restore( - idsOrFilter: string[] | InternalFilterQuery, - sharedContext?: Context - ): Promise<[TEntity[], Record]> - upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - sharedContext?: Context - ): Promise -} - -export function abstractServiceFactory< - TContainer extends object = object, - TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any }, - TFilters extends { [K in FilterableMethods]?: any } = { - [K in FilterableMethods]?: any - } ->( - model: new (...args: any[]) => any -): { - new (container: TContainer): AbstractService< - TEntity, - TContainer, - TDTOs, - TFilters - > -} { - const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository` - const propertyRepositoryName = `__${injectedRepositoryName}__` - - class AbstractService_ - implements AbstractService - { - readonly __container__: TContainer; - [key: string]: any - - constructor(container: TContainer) { - this.__container__ = container - this[propertyRepositoryName] = container[injectedRepositoryName] - } - - static retrievePrimaryKeys(entity: EntityClass | EntitySchema) { - return ( - (entity as EntitySchema).meta?.primaryKeys ?? - (entity as EntityClass).prototype.__meta?.primaryKeys ?? ["id"] - ) - } - - @InjectManager(propertyRepositoryName) - async retrieve( - primaryKeyValues: string | string[] | object[], - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const primaryKeys = AbstractService_.retrievePrimaryKeys(model) - - if (!isDefined(primaryKeyValues)) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `${ - primaryKeys.length === 1 - ? `"${ - lowerCaseFirst(model.name) + upperCaseFirst(primaryKeys[0]) - }"` - : `${lowerCaseFirst(model.name)} ${primaryKeys.join(", ")}` - } must be defined` - ) - } - - let primaryKeysCriteria = {} - if (primaryKeys.length === 1) { - primaryKeysCriteria[primaryKeys[0]] = primaryKeyValues - } else { - primaryKeysCriteria = (primaryKeyValues as string[] | object[]).map( - (primaryKeyValue) => ({ - $and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })), - }) - ) - } - - const queryOptions = buildQuery(primaryKeysCriteria, config) - - const entities = await this[propertyRepositoryName].find( - queryOptions, - sharedContext - ) - - if (!entities?.length) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `${model.name} with ${primaryKeys.join(", ")}: ${ - Array.isArray(primaryKeyValues) - ? primaryKeyValues.map((v) => - [isString(v) ? v : Object.values(v)].join(", ") - ) - : primaryKeyValues - } was not found` - ) - } - - return entities[0] - } - - @InjectManager(propertyRepositoryName) - async list( - filters: TFilters["list"] = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const queryOptions = buildQuery(filters, config) - - return (await this[propertyRepositoryName].find( - queryOptions, - sharedContext - )) as TEntity[] - } - - @InjectManager(propertyRepositoryName) - async listAndCount( - filters: TFilters["listAndCount"] = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], number]> { - const queryOptions = buildQuery(filters, config) - - return (await this[propertyRepositoryName].findAndCount( - queryOptions, - sharedContext - )) as [TEntity[], number] - } - - @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) - async create( - data: TDTOs["create"][], - @MedusaContext() sharedContext: Context = {} - ): Promise { - return (await this[propertyRepositoryName].create( - data, - sharedContext - )) as TEntity[] - } - - @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) - async update( - data: TDTOs["update"][], - @MedusaContext() sharedContext: Context = {} - ): Promise { - return (await this[propertyRepositoryName].update( - data, - sharedContext - )) as TEntity[] - } - - @InjectTransactionManager(doNotForceTransaction, propertyRepositoryName) - async delete( - primaryKeyValues: string[] | object[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this[propertyRepositoryName].delete(primaryKeyValues, sharedContext) - } - - @InjectTransactionManager(propertyRepositoryName) - async softDelete( - idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this[propertyRepositoryName].softDelete( - idsOrFilter, - sharedContext - ) - } - - @InjectTransactionManager(propertyRepositoryName) - async restore( - idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this[propertyRepositoryName].restore( - idsOrFilter, - sharedContext - ) - } - - @InjectTransactionManager(propertyRepositoryName) - async upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - return await this[propertyRepositoryName].upsert(data, sharedContext) - } - } - - return AbstractService_ as unknown as new ( - container: TContainer - ) => AbstractService -} diff --git a/packages/utils/src/modules-sdk/decorators/inject-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-manager.ts index 0b40acfd1dcbd..d83c1deee40fc 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-manager.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-manager.ts @@ -1,4 +1,5 @@ import { Context } from "@medusajs/types" +import { MedusaContextType } from "./context-parameter" export function InjectManager(managerProperty?: string): MethodDecorator { return function ( @@ -37,8 +38,14 @@ export function InjectManager(managerProperty?: string): MethodDecorator { ? this : this[managerProperty] - copiedContext.manager ??= resourceWithManager.getFreshManager() - copiedContext.transactionManager ??= originalContext?.transactionManager + copiedContext.manager = + originalContext.manager ?? resourceWithManager.getFreshManager() + + if (originalContext?.transactionManager) { + copiedContext.transactionManager = originalContext?.transactionManager + } + + copiedContext.__type = MedusaContextType args[argIndex] = copiedContext diff --git a/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts index 4387cfb982b5e..580304467e543 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts @@ -60,9 +60,13 @@ export function InjectTransactionManager( }) } - copiedContext.transactionManager ??= transactionManager - copiedContext.manager ??= originalContext?.manager - copiedContext.__type ??= MedusaContextType + copiedContext.transactionManager = transactionManager + + if (originalContext?.manager) { + copiedContext.manager = originalContext?.manager + } + + copiedContext.__type = MedusaContextType args[argIndex] = copiedContext diff --git a/packages/utils/src/modules-sdk/index.ts b/packages/utils/src/modules-sdk/index.ts index f4c31ebb23f4c..8837a26b11e25 100644 --- a/packages/utils/src/modules-sdk/index.ts +++ b/packages/utils/src/modules-sdk/index.ts @@ -5,4 +5,5 @@ export * from "./loaders/mikro-orm-connection-loader" export * from "./loaders/container-loader-factory" export * from "./create-pg-connection" export * from "./migration-scripts" -export * from "./abstract-service-factory" +export * from "./internal-module-service-factory" +export * from "./abstract-module-service-factory" diff --git a/packages/utils/src/modules-sdk/internal-module-service-factory.ts b/packages/utils/src/modules-sdk/internal-module-service-factory.ts new file mode 100644 index 0000000000000..62b2039a2010f --- /dev/null +++ b/packages/utils/src/modules-sdk/internal-module-service-factory.ts @@ -0,0 +1,442 @@ +import { + BaseFilterable, + Context, + FilterQuery, + FilterQuery as InternalFilterQuery, + FindConfig, + ModulesSdkTypes, +} from "@medusajs/types" +import { EntitySchema } from "@mikro-orm/core" +import { EntityClass } from "@mikro-orm/core/typings" +import { + doNotForceTransaction, + isDefined, + isObject, + isString, + lowerCaseFirst, + MedusaError, + shouldForceTransaction, +} from "../common" +import { buildQuery } from "./build-query" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "./decorators" + +type SelectorAndData = { + selector: FilterQuery | BaseFilterable> + data: any +} + +export function internalModuleServiceFactory< + TContainer extends object = object +>( + model: any +): { + new ( + container: TContainer + ): ModulesSdkTypes.InternalModuleService +} { + const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository` + const propertyRepositoryName = `__${injectedRepositoryName}__` + + class AbstractService_ + implements ModulesSdkTypes.InternalModuleService + { + readonly __container__: TContainer; + [key: string]: any + + constructor(container: TContainer) { + this.__container__ = container + this[propertyRepositoryName] = container[injectedRepositoryName] + } + + static retrievePrimaryKeys(entity: EntityClass | EntitySchema) { + return ( + (entity as EntitySchema).meta?.primaryKeys ?? + (entity as EntityClass).prototype.__meta?.primaryKeys ?? ["id"] + ) + } + + static buildUniqueCompositeKeyValue(keys: string[], data: object) { + return keys.map((k) => data[k]).join("_") + } + + @InjectManager(propertyRepositoryName) + async retrieve( + idOrObject: string | object, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) + + if ( + !isDefined(idOrObject) || + (isString(idOrObject) && primaryKeys.length > 1) || + ((!isString(idOrObject) || + (isObject(idOrObject) && !idOrObject[primaryKeys[0]])) && + primaryKeys.length === 1) + ) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${ + primaryKeys.length === 1 + ? `${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}` + : `${lowerCaseFirst(model.name)} - ${primaryKeys.join(", ")}` + } must be defined` + ) + } + + let primaryKeysCriteria = {} + if (primaryKeys.length === 1) { + primaryKeysCriteria[primaryKeys[0]] = idOrObject + } else { + const idOrObject_ = Array.isArray(idOrObject) + ? idOrObject + : [idOrObject] + primaryKeysCriteria = idOrObject_.map((primaryKeyValue) => ({ + $and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })), + })) + } + + const queryOptions = buildQuery(primaryKeysCriteria, config) + + const entities = await this[propertyRepositoryName].find( + queryOptions, + sharedContext + ) + + if (!entities?.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${model.name} with ${primaryKeys.join(", ")}: ${ + Array.isArray(idOrObject) + ? idOrObject.map((v) => + [isString(v) ? v : Object.values(v)].join(", ") + ) + : idOrObject + } was not found` + ) + } + + return entities[0] + } + + @InjectManager(propertyRepositoryName) + async list( + filters: FilterQuery | BaseFilterable> = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const queryOptions = buildQuery(filters, config) + + return await this[propertyRepositoryName].find( + queryOptions, + sharedContext + ) + } + + @InjectManager(propertyRepositoryName) + async listAndCount( + filters: FilterQuery | BaseFilterable> = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], number]> { + const queryOptions = buildQuery(filters, config) + + return await this[propertyRepositoryName].findAndCount( + queryOptions, + sharedContext + ) + } + + create(data: any, sharedContext?: Context): Promise + create(data: any[], sharedContext?: Context): Promise + + @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) + async create( + data: any | any[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + if (!isDefined(data) || (Array.isArray(data) && data.length === 0)) { + return (Array.isArray(data) ? [] : void 0) as TEntity | TEntity[] + } + + const data_ = Array.isArray(data) ? data : [data] + const entities = await this[propertyRepositoryName].create( + data_, + sharedContext + ) + + return Array.isArray(data) ? entities : entities[0] + } + + update(data: any[], sharedContext?: Context): Promise + update(data: any, sharedContext?: Context): Promise + update( + selectorAndData: SelectorAndData, + sharedContext?: Context + ): Promise + update( + selectorAndData: SelectorAndData[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) + async update( + input: any | any[] | SelectorAndData | SelectorAndData[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + if (!isDefined(input) || (Array.isArray(input) && input.length === 0)) { + return (Array.isArray(input) ? [] : void 0) as TEntity | TEntity[] + } + + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) + const inputArray = Array.isArray(input) ? input : [input] + + const toUpdateData: { entity; update }[] = [] + + // Only used when we receive data and no selector + const keySelectorForDataOnly: any = { + $or: [], + } + const keySelectorDataMap = new Map() + + for (const input_ of inputArray) { + if (input_.selector) { + const entitiesToUpdate = await this.list( + input_.selector, + {}, + sharedContext + ) + // Create a pair of entity and data to update + entitiesToUpdate.forEach((entity) => { + toUpdateData.push({ + entity, + update: input_.data, + }) + }) + } else { + // in case we are manipulating the data, then extract the primary keys as a selector and the rest as the data to update + const selector = {} + + primaryKeys.forEach((key) => { + selector[key] = input_[key] + }) + + const uniqueCompositeKey = + AbstractService_.buildUniqueCompositeKeyValue(primaryKeys, input_) + keySelectorDataMap.set(uniqueCompositeKey, input_) + + keySelectorForDataOnly.$or.push(selector) + } + } + + if (keySelectorForDataOnly.$or.length) { + const entitiesToUpdate = await this.list( + keySelectorForDataOnly, + {}, + sharedContext + ) + + // Create a pair of entity and data to update + entitiesToUpdate.forEach((entity) => { + const uniqueCompositeKey = + AbstractService_.buildUniqueCompositeKeyValue(primaryKeys, entity) + toUpdateData.push({ + entity, + update: keySelectorDataMap.get(uniqueCompositeKey)!, + }) + }) + + // Only throw for missing entities when we dont have selectors involved as selector by design can return 0 entities + if (entitiesToUpdate.length !== keySelectorDataMap.size) { + const entityName = (model as EntityClass).name ?? model + + const compositeKeysValuesForFoundEntities = new Set( + entitiesToUpdate.map((entity) => { + return AbstractService_.buildUniqueCompositeKeyValue( + primaryKeys, + entity + ) + }) + ) + + const missingEntityValues: any[] = [] + + ;[...keySelectorDataMap.keys()].filter((key) => { + if (!compositeKeysValuesForFoundEntities.has(key)) { + const value = key.replace(/_/gi, " - ") + missingEntityValues.push(value) + } + }) + + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${entityName} with ${primaryKeys.join( + ", " + )} "${missingEntityValues.join(", ")}" not found` + ) + } + } + + return await this[propertyRepositoryName].update( + toUpdateData, + sharedContext + ) + } + + delete(idOrSelector: string, sharedContext?: Context): Promise + delete(idOrSelector: string[], sharedContext?: Context): Promise + delete(idOrSelector: object, sharedContext?: Context): Promise + delete(idOrSelector: object[], sharedContext?: Context): Promise + delete( + idOrSelector: { + selector: FilterQuery | BaseFilterable> + }, + sharedContext?: Context + ): Promise + + @InjectTransactionManager(doNotForceTransaction, propertyRepositoryName) + async delete( + idOrSelector: + | string + | string[] + | object + | object[] + | { + selector: FilterQuery | BaseFilterable> + }, + @MedusaContext() sharedContext: Context = {} + ): Promise { + if ( + !isDefined(idOrSelector) || + (Array.isArray(idOrSelector) && idOrSelector.length === 0) + ) { + return + } + + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) + + if ( + (Array.isArray(idOrSelector) && idOrSelector.length === 0) || + ((isString(idOrSelector) || + (Array.isArray(idOrSelector) && isString(idOrSelector[0]))) && + primaryKeys.length > 1) + ) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${ + primaryKeys.length === 1 + ? `"${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}"` + : `${lowerCaseFirst(model.name)} - ${primaryKeys.join(", ")}` + } must be defined` + ) + } + + const deleteCriteria: any = { + $or: [], + } + + if (isObject(idOrSelector) && "selector" in idOrSelector) { + const entitiesToDelete = await this.list( + idOrSelector.selector as FilterQuery, + { + select: primaryKeys, + }, + sharedContext + ) + + for (const entity of entitiesToDelete) { + const criteria = {} + primaryKeys.forEach((key) => { + criteria[key] = entity[key] + }) + deleteCriteria.$or.push(criteria) + } + } else { + const primaryKeysValues = Array.isArray(idOrSelector) + ? idOrSelector + : [idOrSelector] + + deleteCriteria.$or = primaryKeysValues.map((primaryKeyValue) => { + const criteria = {} + + if (isObject(primaryKeyValue)) { + Object.entries(primaryKeyValue).forEach(([key, value]) => { + criteria[key] = value + }) + } else { + criteria[primaryKeys[0]] = primaryKeyValue + } + + // TODO: Revisit + /*primaryKeys.forEach((key) => { + /!*if ( + isObject(primaryKeyValue) && + !isDefined(primaryKeyValue[key]) && + // primaryKeys.length > 1 + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Composite key must contain all primary key fields: ${primaryKeys.join( + ", " + )}. Found: ${Object.keys(primaryKeyValue)}` + ) + }*!/ + + criteria[key] = isObject(primaryKeyValue) + ? primaryKeyValue[key] + : primaryKeyValue + })*/ + return criteria + }) + } + + await this[propertyRepositoryName].delete(deleteCriteria, sharedContext) + } + + @InjectTransactionManager(propertyRepositoryName) + async softDelete( + idsOrFilter: string[] | InternalFilterQuery, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this[propertyRepositoryName].softDelete( + idsOrFilter, + sharedContext + ) + } + + @InjectTransactionManager(propertyRepositoryName) + async restore( + idsOrFilter: string[] | InternalFilterQuery, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this[propertyRepositoryName].restore( + idsOrFilter, + sharedContext + ) + } + + upsert(data: any[], sharedContext?: Context): Promise + upsert(data: any, sharedContext?: Context): Promise + + @InjectTransactionManager(propertyRepositoryName) + async upsert( + data: any | any[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const entities = await this[propertyRepositoryName].upsert( + data_, + sharedContext + ) + return Array.isArray(data) ? entities : entities[0] + } + } + + return AbstractService_ as unknown as new ( + container: TContainer + ) => ModulesSdkTypes.InternalModuleService +} diff --git a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts index 3d48981505b6f..e89cca934a78f 100644 --- a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts +++ b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts @@ -8,7 +8,7 @@ import { } from "@medusajs/types" import { lowerCaseFirst } from "../../common" import { asClass } from "awilix" -import { abstractServiceFactory } from "../abstract-service-factory" +import { internalModuleServiceFactory } from "../internal-module-service-factory" import { mikroOrmBaseRepositoryFactory } from "../../dal" type RepositoryLoaderOptions = { @@ -96,7 +96,7 @@ export function loadModuleServices({ const finalService = moduleServicesMap.get(mappedServiceName) if (!finalService) { - moduleServicesMap.set(mappedServiceName, abstractServiceFactory(Model)) + moduleServicesMap.set(mappedServiceName, internalModuleServiceFactory(Model)) } }) diff --git a/packages/workflow-engine-inmemory/src/services/index.ts b/packages/workflow-engine-inmemory/src/services/index.ts index 5a6d313d860b3..75bcf7eb47f97 100644 --- a/packages/workflow-engine-inmemory/src/services/index.ts +++ b/packages/workflow-engine-inmemory/src/services/index.ts @@ -1,3 +1,2 @@ -export * from "./workflow-execution" export * from "./workflow-orchestrator" export * from "./workflows-module" diff --git a/packages/workflow-engine-inmemory/src/services/workflow-execution.ts b/packages/workflow-engine-inmemory/src/services/workflow-execution.ts deleted file mode 100644 index 158557ec0bae8..0000000000000 --- a/packages/workflow-engine-inmemory/src/services/workflow-execution.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { WorkflowExecution } from "@models" - -type InjectedDependencies = { - workflowExecutionRepository: DAL.RepositoryService -} - -export class WorkflowExecutionService< - TEntity extends WorkflowExecution = WorkflowExecution -> extends ModulesSdkUtils.abstractServiceFactory( - WorkflowExecution -) { - protected workflowExecutionRepository_: DAL.RepositoryService - - constructor({ workflowExecutionRepository }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.workflowExecutionRepository_ = workflowExecutionRepository - } -} diff --git a/packages/workflow-engine-inmemory/src/services/workflows-module.ts b/packages/workflow-engine-inmemory/src/services/workflows-module.ts index 31be5674d58a3..789fa5760092b 100644 --- a/packages/workflow-engine-inmemory/src/services/workflows-module.ts +++ b/packages/workflow-engine-inmemory/src/services/workflows-module.ts @@ -4,8 +4,8 @@ import { FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, } from "@medusajs/types" -import {} from "@medusajs/types/src" import { InjectManager, InjectSharedContext, @@ -16,15 +16,12 @@ import type { UnwrapWorkflowInputDataType, WorkflowOrchestratorTypes, } from "@medusajs/workflows-sdk" -import { - WorkflowExecutionService, - WorkflowOrchestratorService, -} from "@services" +import { WorkflowOrchestratorService } from "@services" import { joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService workflowOrchestratorService: WorkflowOrchestratorService } @@ -32,7 +29,7 @@ export class WorkflowsModuleService implements WorkflowOrchestratorTypes.IWorkflowsModuleService { protected baseRepository_: DAL.RepositoryService - protected workflowExecutionService_: WorkflowExecutionService + protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService protected workflowOrchestratorService_: WorkflowOrchestratorService constructor( @@ -64,7 +61,7 @@ export class WorkflowsModuleService sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< WorkflowOrchestratorTypes.WorkflowExecutionDTO[] >(wfExecutions, { populate: true, diff --git a/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts b/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts index 7254f3b90dc2d..60c9771def6b7 100644 --- a/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts +++ b/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts @@ -5,14 +5,12 @@ import { TransactionStep, } from "@medusajs/orchestration" import { TransactionState } from "@medusajs/utils" -import { - WorkflowExecutionService, - WorkflowOrchestratorService, -} from "@services" +import { WorkflowOrchestratorService } from "@services" +import { ModulesSdkTypes } from "@medusajs/types" // eslint-disable-next-line max-len export class InMemoryDistributedTransactionStorage extends DistributedTransactionStorage { - private workflowExecutionService_: WorkflowExecutionService + private workflowExecutionService_: ModulesSdkTypes.InternalModuleService private workflowOrchestratorService_: WorkflowOrchestratorService private storage: Map = new Map() @@ -22,7 +20,7 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio constructor({ workflowExecutionService, }: { - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService }) { super() diff --git a/packages/workflow-engine-redis/src/services/index.ts b/packages/workflow-engine-redis/src/services/index.ts index 5a6d313d860b3..75bcf7eb47f97 100644 --- a/packages/workflow-engine-redis/src/services/index.ts +++ b/packages/workflow-engine-redis/src/services/index.ts @@ -1,3 +1,2 @@ -export * from "./workflow-execution" export * from "./workflow-orchestrator" export * from "./workflows-module" diff --git a/packages/workflow-engine-redis/src/services/workflow-execution.ts b/packages/workflow-engine-redis/src/services/workflow-execution.ts deleted file mode 100644 index 158557ec0bae8..0000000000000 --- a/packages/workflow-engine-redis/src/services/workflow-execution.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { WorkflowExecution } from "@models" - -type InjectedDependencies = { - workflowExecutionRepository: DAL.RepositoryService -} - -export class WorkflowExecutionService< - TEntity extends WorkflowExecution = WorkflowExecution -> extends ModulesSdkUtils.abstractServiceFactory( - WorkflowExecution -) { - protected workflowExecutionRepository_: DAL.RepositoryService - - constructor({ workflowExecutionRepository }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.workflowExecutionRepository_ = workflowExecutionRepository - } -} diff --git a/packages/workflow-engine-redis/src/services/workflows-module.ts b/packages/workflow-engine-redis/src/services/workflows-module.ts index 31be5674d58a3..6667236626790 100644 --- a/packages/workflow-engine-redis/src/services/workflows-module.ts +++ b/packages/workflow-engine-redis/src/services/workflows-module.ts @@ -4,8 +4,8 @@ import { FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, } from "@medusajs/types" -import {} from "@medusajs/types/src" import { InjectManager, InjectSharedContext, @@ -16,15 +16,12 @@ import type { UnwrapWorkflowInputDataType, WorkflowOrchestratorTypes, } from "@medusajs/workflows-sdk" -import { - WorkflowExecutionService, - WorkflowOrchestratorService, -} from "@services" -import { joinerConfig } from "../joiner-config" +import {WorkflowOrchestratorService} from "@services" +import {joinerConfig} from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService workflowOrchestratorService: WorkflowOrchestratorService } @@ -32,7 +29,7 @@ export class WorkflowsModuleService implements WorkflowOrchestratorTypes.IWorkflowsModuleService { protected baseRepository_: DAL.RepositoryService - protected workflowExecutionService_: WorkflowExecutionService + protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService protected workflowOrchestratorService_: WorkflowOrchestratorService constructor( @@ -64,7 +61,7 @@ export class WorkflowsModuleService sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< WorkflowOrchestratorTypes.WorkflowExecutionDTO[] >(wfExecutions, { populate: true, diff --git a/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts b/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts index 533181cf7f8fc..512c8e7cf6760 100644 --- a/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts +++ b/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts @@ -5,8 +5,8 @@ import { TransactionStep, } from "@medusajs/orchestration" import { TransactionState } from "@medusajs/utils" +import { ModulesSdkTypes } from "@medusajs/types" import { - WorkflowExecutionService, WorkflowOrchestratorService, } from "@services" import { Queue, Worker } from "bullmq" @@ -21,7 +21,7 @@ enum JobType { // eslint-disable-next-line max-len export class RedisDistributedTransactionStorage extends DistributedTransactionStorage { private static TTL_AFTER_COMPLETED = 60 * 15 // 15 minutes - private workflowExecutionService_: WorkflowExecutionService + private workflowExecutionService_: ModulesSdkTypes.InternalModuleService private workflowOrchestratorService_: WorkflowOrchestratorService private redisClient: Redis @@ -34,7 +34,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt redisWorkerConnection, redisQueueName, }: { - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService, redisConnection: Redis redisWorkerConnection: Redis redisQueueName: string From 8cbf6c60fec7fe8ddf59dcf420b9339f84b8636c Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:56:55 +0100 Subject: [PATCH 4/4] feat(dashboard): DataTable component (#6297) --- .changeset/healthy-ligers-learn.md | 6 + .eslintignore | 2 + .eslintrc.js | 43 ++- packages/admin-next/dashboard/package.json | 4 +- .../public/locales/en/translation.json | 35 +- .../common/order-table-cells/index.ts | 1 - .../order-table-cells/order-table-cells.tsx | 97 ------ .../src/components/layout/shell/shell.tsx | 16 +- .../data-table/data-table-filter/context.tsx | 19 ++ .../data-table-filter/data-table-filter.tsx | 255 ++++++++++++++ .../data-table-filter/date-filter.tsx | 322 ++++++++++++++++++ .../data-table/data-table-filter/index.ts | 1 + .../data-table-filter/select-filter.tsx | 261 ++++++++++++++ .../data-table/data-table-filter/types.ts | 8 + .../data-table-order-by.tsx | 157 +++++++++ .../data-table/data-table-order-by/index.ts | 1 + .../data-table-query/data-table-query.tsx | 32 ++ .../data-table/data-table-query/index.ts | 1 + .../data-table-root/data-table-root.tsx | 272 +++++++++++++++ .../table/data-table/data-table-root/index.ts | 1 + .../data-table-search/data-table-search.tsx | 57 ++++ .../data-table/data-table-search/index.ts | 1 + .../data-table-skeleton.tsx | 115 +++++++ .../data-table/data-table-skeleton/index.ts | 1 + .../table/data-table/data-table.tsx | 74 ++++ .../src/components/table/data-table/hooks.tsx | 73 ++++ .../src/components/table/data-table/index.ts | 2 + .../common/date-cell/date-cell.tsx | 41 +++ .../table-cells/common/date-cell/index.ts | 1 + .../table-cells/common/status-cell/index.ts | 1 + .../common/status-cell/status-cell.tsx | 32 ++ .../order/customer-cell/customer-cell.tsx | 29 ++ .../table-cells/order/customer-cell/index.ts | 1 + .../order/display-id-cell/display-id-cell.tsx | 19 ++ .../order/display-id-cell/index.ts | 1 + .../fulfillment-status-cell.tsx | 46 +++ .../order/fulfillment-status-cell/index.ts | 1 + .../table-cells/order/items-cell/index.ts | 1 + .../order/items-cell/items-cell.tsx | 26 ++ .../order/payment-status-cell/index.ts | 1 + .../payment-status-cell.tsx | 33 ++ .../order/sales-channel-cell/index.ts | 1 + .../sales-channel-cell/sales-channel-cell.tsx | 30 ++ .../table-cells/order/total-cell/index.ts | 1 + .../order/total-cell/total-cell.tsx | 44 +++ .../table/columns/use-order-table-columns.tsx | 133 ++++++++ .../table/filters/use-order-table-filters.tsx | 154 +++++++++ .../table/query/use-order-table-query.tsx | 58 ++++ .../dashboard/src/hooks/use-data-table.tsx | 112 ++++++ .../dashboard/src/hooks/use-query-params.tsx | 16 +- .../router-provider/router-provider.tsx | 2 +- .../customer-order-section.tsx | 232 ++----------- .../dashboard/src/routes/orders/list/index.ts | 1 - .../dashboard/src/routes/orders/list/list.tsx | 11 - .../components/order-list-table/index.ts | 1 + .../order-list-table/order-list-table.tsx | 67 ++++ .../src/routes/orders/order-list/index.ts | 1 + .../routes/orders/order-list/order-list.tsx | 9 + packages/design-system/ui-preset/package.json | 2 +- packages/design-system/ui/package.json | 2 +- .../ui/src/components/table/table.tsx | 8 +- yarn.lock | 94 ++--- 62 files changed, 2697 insertions(+), 372 deletions(-) create mode 100644 .changeset/healthy-ligers-learn.md delete mode 100644 packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts delete mode 100644 packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/data-table/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/use-data-table.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/orders/list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/orders/list/list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-list/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx diff --git a/.changeset/healthy-ligers-learn.md b/.changeset/healthy-ligers-learn.md new file mode 100644 index 0000000000000..6f0a2a908194b --- /dev/null +++ b/.changeset/healthy-ligers-learn.md @@ -0,0 +1,6 @@ +--- +"@medusajs/ui-preset": patch +"@medusajs/ui": patch +--- + +feat(ui,ui-preset): Update to latest version of TailwindCSS. Increase spacing between columns in component. diff --git a/.eslintignore b/.eslintignore index f7f026ec94781..a2f3463632feb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,8 @@ packages/* !packages/medusa !packages/admin-ui !packages/admin +!packages/admin-next +!packages/admin-next/dashboard !packages/medusa-payment-stripe !packages/medusa-payment-paypal !packages/event-bus-redis diff --git a/.eslintrc.js b/.eslintrc.js index c34c47f13a79a..56edede0b3e47 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,7 +72,6 @@ module.exports = { node: true, jest: true, }, - ignorePatterns: ["packages/admin-next/dashboard/**/dist"], overrides: [ { files: ["*.ts"], @@ -86,6 +85,7 @@ module.exports = { "./packages/medusa-payment-paypal/tsconfig.spec.json", "./packages/admin-ui/tsconfig.json", "./packages/admin-ui/tsconfig.spec.json", + "./packages/admin-next/dashboard/tsconfig.json", "./packages/event-bus-local/tsconfig.spec.json", "./packages/event-bus-redis/tsconfig.spec.json", "./packages/medusa-plugin-meilisearch/tsconfig.spec.json", @@ -228,23 +228,52 @@ module.exports = { }, }, { - files: ["packages/admin-next/dashboard/src/**/*.{ts,tsx}"], - env: { browser: true, es2020: true, node: true }, + files: [ + "packages/admin-next/dashboard/**/*.ts", + "packages/admin-next/dashboard/**/*.tsx", + ], + plugins: ["unused-imports", "react-refresh"], extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", "plugin:react-hooks/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { - project: "tsconfig.json", + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: "module", // Allows for the use of imports + project: "./packages/admin-next/dashboard/tsconfig.json", + }, + globals: { + __BASE__: "readonly", + }, + env: { + browser: true, }, - plugins: ["react-refresh"], rules: { + "prettier/prettier": "error", + "react/prop-types": "off", + "new-cap": "off", + "require-jsdoc": "off", + "valid-jsdoc": "off", "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], + "no-unused-expressions": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], }, }, { diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json index 537059c726d30..db37624a6a5f2 100644 --- a/packages/admin-next/dashboard/package.json +++ b/packages/admin-next/dashboard/package.json @@ -22,6 +22,7 @@ "@medusajs/icons": "workspace:^", "@medusajs/ui": "workspace:^", "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-hover-card": "^1.0.7", "@tanstack/react-query": "4.22.0", "@tanstack/react-table": "8.10.7", "@uiw/react-json-view": "2.0.0-alpha.10", @@ -43,13 +44,14 @@ "@medusajs/types": "workspace:^", "@medusajs/ui-preset": "workspace:^", "@medusajs/vite-plugin-extension": "workspace:^", + "@types/node": "^20.11.15", "@types/react": "18.2.43", "@types/react-dom": "18.2.17", "@vitejs/plugin-react": "4.2.1", "autoprefixer": "10.4.16", "postcss": "8.4.32", "prettier": "^3.1.1", - "tailwindcss": "3.3.6", + "tailwindcss": "^3.4.1", "typescript": "5.2.2", "vite": "5.0.10" }, diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json index 7e8bee990e87d..713a2c3355155 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en/translation.json @@ -23,6 +23,7 @@ "pages": "pages", "next": "Next", "prev": "Prev", + "is": "is", "extensions": "Extensions", "settings": "Settings", "general": "General", @@ -35,6 +36,8 @@ "remove": "Remove", "admin": "Admin", "store": "Store", + "items_one": "{{count}} item", + "items_other": "{{count}} items", "countSelected": "{{count}} selected", "plusCountMore": "+ {{count}} more", "areYouSure": "Are you sure?", @@ -107,7 +110,29 @@ "deleteCustomerGroupWarning": "You are about to delete the customer group {{name}}. This action cannot be undone." }, "orders": { - "domain": "Orders" + "domain": "Orders", + "paymentStatusLabel": "Payment Status", + "paymentStatus": { + "notPaid": "Not Paid", + "awaiting": "Awaiting", + "captured": "Captured", + "partiallyRefunded": "Partially Refunded", + "refunded": "Refunded", + "canceled": "Canceled", + "requresAction": "Requires Action" + }, + "fulfillmentStatusLabel": "Fulfillment Status", + "fulfillmentStatus": { + "notFulfilled": "Not Fulfilled", + "partiallyFulfilled": "Partially Fulfilled", + "fulfilled": "Fulfilled", + "partiallyShipped": "Partially Shipped", + "shipped": "Shipped", + "partiallyReturned": "Partially Returned", + "returned": "Returned", + "canceled": "Canceled", + "requresAction": "Requires Action" + } }, "draftOrders": { "domain": "Draft Orders" @@ -263,6 +288,14 @@ "total": "Total", "created": "Created", "key": "Key", + "customer": "Customer", + "date": "Date", + "order": "Order", + "fulfillment": "Fulfillment", + "payment": "Payment", + "items": "Items", + "salesChannel": "Sales Channel", + "region": "Region", "role": "Role", "sent": "Sent" } diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts b/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts deleted file mode 100644 index 20b29f6c8514a..0000000000000 --- a/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./order-table-cells" diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx b/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx deleted file mode 100644 index 9026e455903a1..0000000000000 --- a/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { Order } from "@medusajs/medusa" -import { StatusBadge } from "@medusajs/ui" -import { format } from "date-fns" -import { getPresentationalAmount } from "../../../lib/money-amount-helpers" - -export const OrderDisplayIdCell = ({ id }: { id: Order["display_id"] }) => { - return #{id} -} - -export const OrderDateCell = ({ - date, -}: { - date: Order["created_at"] | string -}) => { - const value = new Date(date) - - return {format(value, "dd MMM, yyyy")} -} - -export const OrderFulfillmentStatusCell = ({ - status, -}: { - status: Order["fulfillment_status"] -}) => { - switch (status) { - case "not_fulfilled": - return Not fulfilled - case "partially_fulfilled": - return Partially fulfilled - case "fulfilled": - return Fulfilled - case "partially_shipped": - return Partially shipped - case "shipped": - return Shipped - case "partially_returned": - return Partially returned - case "returned": - return Returned - case "canceled": - return Canceled - case "requires_action": - return Requires action - } -} - -export const OrderPaymentStatusCell = ({ - status, -}: { - status: Order["payment_status"] -}) => { - switch (status) { - case "not_paid": - return Not paid - case "awaiting": - return Awaiting - case "captured": - return Captured - case "partially_refunded": - return Partially refunded - case "refunded": - return Refunded - case "canceled": - return Canceled - case "requires_action": - return Requires action - } -} - -// TODO: Fix formatting amount with correct division eg. EUR 1000 -> EUR 10.00 -// Source currency info from `@medusajs/medusa` definition -export const OrderTotalCell = ({ - total, - currencyCode, -}: { - total: Order["total"] - currencyCode: Order["currency_code"] -}) => { - const formatted = new Intl.NumberFormat(undefined, { - style: "currency", - currency: currencyCode, - currencyDisplay: "narrowSymbol", - }).format(0) - - const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() - - const presentationAmount = getPresentationalAmount(total, currencyCode) - const formattedTotal = new Intl.NumberFormat(undefined, { - style: "decimal", - }).format(presentationAmount) - - return ( - - {symbol} {formattedTotal} {currencyCode.toUpperCase()} - - ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx index 31044999a7196..7150224afb904 100644 --- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -35,13 +35,13 @@ export const Shell = ({ children }: PropsWithChildren) => { {children}{children} -
+
-
+
-
+
) @@ -76,6 +76,7 @@ const Breadcrumbs = () => {
    {crumbs.map((crumb, index) => { const isLast = index === crumbs.length - 1 + const isSingle = crumbs.length === 1 return (
  1. { ) : (
    - ... - + {!isSingle && ...} + {crumb.label}
    diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx new file mode 100644 index 0000000000000..daacb414408fe --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from "react" + +type DataTableFilterContextValue = { + removeFilter: (key: string) => void + removeAllFilters: () => void +} + +export const DataTableFilterContext = + createContext(null) + +export const useDataTableFilterContext = () => { + const ctx = useContext(DataTableFilterContext) + if (!ctx) { + throw new Error( + "useDataTableFacetedFilterContext must be used within a DataTableFacetedFilter" + ) + } + return ctx +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx new file mode 100644 index 0000000000000..c5ac5f6be9ae9 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx @@ -0,0 +1,255 @@ +import { Button, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useSearchParams } from "react-router-dom" + +import { DataTableFilterContext, useDataTableFilterContext } from "./context" +import { DateFilter } from "./date-filter" +import { SelectFilter } from "./select-filter" + +type Option = { + label: string + value: unknown +} + +export type Filter = { + key: string + label: string +} & ( + | { + type: "select" + options: Option[] + multiple?: boolean + searchable?: boolean + } + | { + type: "date" + options?: never + } +) + +type DataTableFilterProps = { + filters: Filter[] + prefix?: string +} + +export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { + const [searchParams] = useSearchParams() + const [open, setOpen] = useState(false) + + const [activeFilters, setActiveFilters] = useState( + getInitialFilters({ searchParams, filters, prefix }) + ) + + const availableFilters = filters.filter( + (f) => !activeFilters.find((af) => af.key === f.key) + ) + + /** + * If there are any filters in the URL that are not in the active filters, + * add them to the active filters. This ensures that we display the filters + * if a user navigates to a page with filters in the URL. + */ + const initialMount = useRef(true) + + useEffect(() => { + if (initialMount.current) { + const params = new URLSearchParams(searchParams) + + filters.forEach((filter) => { + const key = prefix ? `${prefix}_${filter.key}` : filter.key + const value = params.get(key) + if (value && !activeFilters.find((af) => af.key === filter.key)) { + console.log("adding filter", filter.key, "to active filters") + if (filter.type === "select") { + setActiveFilters((prev) => [ + ...prev, + { + ...filter, + multiple: filter.multiple, + options: filter.options, + openOnMount: false, + }, + ]) + } else { + setActiveFilters((prev) => [ + ...prev, + { ...filter, openOnMount: false }, + ]) + } + } + }) + } + + initialMount.current = false + }, [activeFilters, filters, prefix, searchParams]) + + const addFilter = (filter: Filter) => { + setOpen(false) + setActiveFilters((prev) => [...prev, { ...filter, openOnMount: true }]) + } + + const removeFilter = useCallback((key: string) => { + setActiveFilters((prev) => prev.filter((f) => f.key !== key)) + }, []) + + const removeAllFilters = useCallback(() => { + setActiveFilters([]) + }, []) + + return ( + ({ + removeFilter, + removeAllFilters, + }), + [removeAllFilters, removeFilter] + )} + > +
    + {activeFilters.map((filter) => { + if (filter.type === "select") { + return ( + + ) + } + + return ( + + ) + })} + {availableFilters.length > 0 && ( + + + + + + { + const hasOpenFilter = activeFilters.find( + (filter) => filter.openOnMount + ) + + if (hasOpenFilter) { + e.preventDefault() + } + }} + > + {availableFilters.map((filter) => { + return ( +
    { + addFilter(filter) + }} + > + {filter.label} +
    + ) + })} +
    +
    +
    + )} + {activeFilters.length > 0 && ( + + )} +
    +
    + ) +} + +type ClearAllFiltersProps = { + filters: Filter[] + prefix?: string +} + +const ClearAllFilters = ({ filters, prefix }: ClearAllFiltersProps) => { + const { removeAllFilters } = useDataTableFilterContext() + const [_, setSearchParams] = useSearchParams() + + const handleRemoveAll = () => { + setSearchParams((prev) => { + const newValues = new URLSearchParams(prev) + + filters.forEach((filter) => { + newValues.delete(prefix ? `${prefix}_${filter.key}` : filter.key) + }) + + return newValues + }) + + removeAllFilters() + } + + return ( + + ) +} + +const getInitialFilters = ({ + searchParams, + filters, + prefix, +}: { + searchParams: URLSearchParams + filters: Filter[] + prefix?: string +}) => { + const params = new URLSearchParams(searchParams) + const activeFilters: (Filter & { openOnMount: boolean })[] = [] + + filters.forEach((filter) => { + const key = prefix ? `${prefix}_${filter.key}` : filter.key + const value = params.get(key) + if (value) { + if (filter.type === "select") { + activeFilters.push({ + ...filter, + multiple: filter.multiple, + options: filter.options, + openOnMount: false, + }) + } else { + activeFilters.push({ ...filter, openOnMount: false }) + } + } + }) + + return activeFilters +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx new file mode 100644 index 0000000000000..fd851b26b2b91 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx @@ -0,0 +1,322 @@ +import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons" +import { DatePicker, Text, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { format } from "date-fns" +import isEqual from "lodash/isEqual" +import { MouseEvent, useState } from "react" + +import { useSelectedParams } from "../hooks" +import { useDataTableFilterContext } from "./context" +import { IFilter } from "./types" + +type DateFilterProps = IFilter + +type DateComparisonOperator = { + gte?: string + lte?: string + lt?: string + gt?: string +} + +export const DateFilter = ({ + filter, + prefix, + openOnMount, +}: DateFilterProps) => { + const [open, setOpen] = useState(openOnMount) + const [showCustom, setShowCustom] = useState(false) + const { key, label } = filter + const { removeFilter } = useDataTableFilterContext() + const selectedParams = useSelectedParams({ param: key, prefix }) + + const handleSelectPreset = (value: DateComparisonOperator) => { + selectedParams.add(JSON.stringify(value)) + setShowCustom(false) + } + + const handleSelectCustom = () => { + selectedParams.delete() + setShowCustom((prev) => !prev) + } + + const currentValue = selectedParams.get() + + const currentDateComparison = parseDateComparison(currentValue) + const customStartValue = getDateFromComparison(currentDateComparison, "gte") + const customEndValue = getDateFromComparison(currentDateComparison, "lte") + + const handleCustomDateChange = ( + value: Date | undefined, + pos: "start" | "end" + ) => { + const key = pos === "start" ? "gte" : "lte" + const dateValue = value ? value.toISOString() : undefined + + selectedParams.add( + JSON.stringify({ + ...(currentDateComparison || {}), + [key]: dateValue, + }) + ) + } + + const getDisplayValueFromPresets = () => { + const preset = presets.find((p) => isEqual(p.value, currentDateComparison)) + return preset?.label + } + + const formatCustomDate = (date: Date | undefined) => { + return date ? format(date, "dd MMM, yyyy") : undefined + } + + const getCustomDisplayValue = () => { + const formattedDates = [customStartValue, customEndValue].map( + formatCustomDate + ) + return formattedDates.filter(Boolean).join(" - ") + } + + const displayValue = getDisplayValueFromPresets() || getCustomDisplayValue() + + const handleRemove = () => { + selectedParams.delete() + removeFilter(key) + } + + let timeoutId: ReturnType | null = null + + const handleOpenChange = (open: boolean) => { + setOpen(open) + + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (!open && !currentValue.length) { + timeoutId = setTimeout(() => { + removeFilter(key) + }, 200) + } + } + + return ( + + + + { + if (e.target instanceof HTMLElement) { + if ( + e.target.attributes.getNamedItem("data-name")?.value === + "filters_menu_content" + ) { + e.preventDefault() + } + } + }} + > +
      + {presets.map((preset) => { + const isSelected = selectedParams + .get() + .includes(JSON.stringify(preset.value)) + return ( +
    • + +
    • + ) + })} +
    • + +
    • +
    + {showCustom && ( +
    +
    +
    + + Starting + +
    +
    + handleCustomDateChange(d, "start")} + /> +
    +
    +
    +
    + + Ending + +
    +
    + { + handleCustomDateChange(d, "end") + }} + /> +
    +
    +
    + )} +
    +
    +
    + ) +} + +type DateDisplayProps = { + label: string + value?: string + onRemove: () => void +} + +const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => { + const handleRemove = (e: MouseEvent) => { + e.stopPropagation() + onRemove() + } + + return ( + +
    +
    + + {label} + +
    + {value && ( +
    +
    + + {value} + +
    +
    + )} + {value && ( +
    + +
    + )} +
    +
    + ) +} + +const today = new Date() +today.setHours(0, 0, 0, 0) + +const presets: { label: string; value: DateComparisonOperator }[] = [ + { + label: "Today", + value: { + gte: today.toISOString(), + }, + }, + { + label: "Last 7 days", + value: { + gte: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago + }, + }, + { + label: "Last 30 days", + value: { + gte: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago + }, + }, + { + label: "Last 90 days", + value: { + gte: new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(), // 90 days ago + }, + }, + { + label: "Last 12 months", + value: { + gte: new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000).toISOString(), // 365 days ago + }, + }, +] + +const parseDateComparison = (value: string[]) => { + return value?.length + ? (JSON.parse(value.join(",")) as DateComparisonOperator) + : null +} + +const getDateFromComparison = ( + comparison: DateComparisonOperator | null, + key: "gte" | "lte" +) => { + return comparison?.[key] ? new Date(comparison[key] as string) : undefined +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts new file mode 100644 index 0000000000000..8363bf9a5a049 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts @@ -0,0 +1 @@ +export * from "./data-table-filter" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx new file mode 100644 index 0000000000000..cf4a1d784f5d1 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx @@ -0,0 +1,261 @@ +import { CheckMini, EllipseMiniSolid, XMarkMini } from "@medusajs/icons" +import { Text, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { Command } from "cmdk" +import { MouseEvent, useState } from "react" +import { useTranslation } from "react-i18next" + +import { useSelectedParams } from "../hooks" +import { useDataTableFilterContext } from "./context" +import { IFilter } from "./types" + +interface SelectFilterProps extends IFilter { + options: { label: string; value: unknown }[] + multiple?: boolean + searchable?: boolean +} + +export const SelectFilter = ({ + filter, + prefix, + multiple, + searchable, + options, + openOnMount, +}: SelectFilterProps) => { + const [open, setOpen] = useState(openOnMount) + const [search, setSearch] = useState("") + const [searchRef, setSearchRef] = useState(null) + + const { t } = useTranslation() + const { removeFilter } = useDataTableFilterContext() + + const { key, label } = filter + const selectedParams = useSelectedParams({ param: key, prefix, multiple }) + const currentValue = selectedParams.get() + + const labelValues = currentValue + .map((v) => options.find((o) => o.value === v)?.label) + .filter(Boolean) as string[] + + const handleRemove = () => { + selectedParams.delete() + removeFilter(key) + } + + let timeoutId: ReturnType | null = null + + const handleOpenChange = (open: boolean) => { + setOpen(open) + + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (!open && !currentValue.length) { + timeoutId = setTimeout(() => { + removeFilter(key) + }, 200) + } + } + + const handleClearSearch = () => { + setSearch("") + if (searchRef) { + searchRef.focus() + } + } + + const handleSelect = (value: unknown) => { + const isSelected = selectedParams.get().includes(String(value)) + + if (isSelected) { + selectedParams.delete(String(value)) + } else { + selectedParams.add(String(value)) + } + } + + return ( + + + + { + if (e.target instanceof HTMLElement) { + if ( + e.target.attributes.getNamedItem("data-name")?.value === + "filters_menu_content" + ) { + e.preventDefault() + e.stopPropagation() + } + } + }} + > + + {searchable && ( +
    +
    + +
    + +
    +
    +
    + )} + + + {t("general.noResultsTitle")} + + + + {options.map((option) => { + const isSelected = selectedParams + .get() + .includes(String(option.value)) + + return ( + { + handleSelect(option.value) + }} + > +
    + {multiple ? : } +
    + {option.label} +
    + ) + })} +
    +
    +
    +
    +
    + ) +} + +type SelectDisplayProps = { + label: string + value?: string | string[] + onRemove: () => void +} + +export const SelectDisplay = ({ + label, + value, + onRemove, +}: SelectDisplayProps) => { + const { t } = useTranslation() + const v = value ? (Array.isArray(value) ? value : [value]) : null + const count = v?.length || 0 + + const handleRemove = (e: MouseEvent) => { + e.stopPropagation() + onRemove() + } + + return ( + +
    +
    0, + } + )} + > + + {label} + +
    +
    + {count > 0 && ( +
    + + {t("general.is")} + +
    + )} + {count > 0 && ( +
    + + {v?.join(", ")} + +
    + )} +
    + {v && v.length > 0 && ( +
    + +
    + )} +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts new file mode 100644 index 0000000000000..d5dfb5e0a896a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts @@ -0,0 +1,8 @@ +export interface IFilter { + filter: { + key: string + label: string + } + openOnMount?: boolean + prefix?: string +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx new file mode 100644 index 0000000000000..43cec0d537f12 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx @@ -0,0 +1,157 @@ +import { ArrowUpDown } from "@medusajs/icons" +import { DropdownMenu, IconButton } from "@medusajs/ui" +import { useState } from "react" +import { useTranslation } from "react-i18next" +import { useSearchParams } from "react-router-dom" + +type DataTableOrderByProps = { + keys: (keyof TData)[] + prefix?: string +} + +enum SortDirection { + ASC = "asc", + DESC = "desc", +} + +type SortState = { + key?: string + dir: SortDirection +} + +const initState = (params: URLSearchParams, prefix?: string): SortState => { + const param = prefix ? `${prefix}_order` : "order" + const sortParam = params.get(param) + + if (!sortParam) { + return { + dir: SortDirection.ASC, + } + } + + const dir = sortParam.startsWith("-") ? SortDirection.DESC : SortDirection.ASC + const key = sortParam.replace("-", "") + + return { + key, + dir, + } +} + +const formatKey = (key: string) => { + const words = key.split("_") + const formattedWords = words.map((word, index) => { + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1) + } else { + return word + } + }) + return formattedWords.join(" ") +} + +export const DataTableOrderBy = ({ + keys, + prefix, +}: DataTableOrderByProps) => { + const [searchParams, setSearchParams] = useSearchParams() + const [state, setState] = useState<{ + key?: string + dir: SortDirection + }>(initState(searchParams, prefix)) + const param = prefix ? `${prefix}_order` : "order" + const { t } = useTranslation() + + const handleDirChange = (dir: string) => { + setState((prev) => ({ + ...prev, + dir: dir as SortDirection, + })) + updateOrderParam({ + key: state.key, + dir: dir as SortDirection, + }) + } + + const handleKeyChange = (value: string) => { + setState((prev) => ({ + ...prev, + key: value, + })) + + updateOrderParam({ + key: value, + dir: state.dir, + }) + } + + const updateOrderParam = (state: SortState) => { + if (!state.key) { + setSearchParams((prev) => { + prev.delete(param) + return prev + }) + + return + } + + const orderParam = + state.dir === SortDirection.ASC ? state.key : `-${state.key}` + setSearchParams((prev) => { + prev.set(param, orderParam) + return prev + }) + } + + return ( + + + + + + + + + {keys.map((key) => { + const stringKey = String(key) + + return ( + event.preventDefault()} + > + {formatKey(stringKey)} + + ) + })} + + + + event.preventDefault()} + > + {t("general.ascending")} + 1 - 30 + + event.preventDefault()} + > + {t("general.descending")} + 30 - 1 + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts new file mode 100644 index 0000000000000..6761c82fb8c51 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts @@ -0,0 +1 @@ +export * from "./data-table-order-by" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx new file mode 100644 index 0000000000000..60cfe1ba0ea0d --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx @@ -0,0 +1,32 @@ +import { Filter } from ".." +import { DataTableFilter } from "../data-table-filter" +import { DataTableOrderBy } from "../data-table-order-by" +import { DataTableSearch } from "../data-table-search" + +export interface DataTableQueryProps { + search?: boolean + orderBy?: (string | number)[] + filters?: Filter[] + prefix?: string +} + +export const DataTableQuery = ({ + search, + orderBy, + filters, + prefix, +}: DataTableQueryProps) => { + return ( +
    +
    + {filters && filters.length > 0 && ( + + )} +
    +
    + {search && } + {orderBy && } +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts new file mode 100644 index 0000000000000..7449807df43c1 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts @@ -0,0 +1 @@ +export * from "./data-table-query" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx new file mode 100644 index 0000000000000..374cc0b2be178 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx @@ -0,0 +1,272 @@ +import { CommandBar, Table, clx } from "@medusajs/ui" +import { + ColumnDef, + Table as ReactTable, + Row, + flexRender, +} from "@tanstack/react-table" +import { ComponentPropsWithoutRef, Fragment, UIEvent, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import { NoResults } from "../../../common/empty-table-content" + +type BulkCommand = { + label: string + shortcut: string + action: (selection: Record) => void +} + +export interface DataTableRootProps { + /** + * The table instance to render + */ + table: ReactTable + /** + * The columns to render + */ + columns: ColumnDef[] + /** + * Function to generate a link to navigate to when clicking on a row + */ + navigateTo?: (row: Row) => string + /** + * Bulk actions to render + */ + commands?: BulkCommand[] + /** + * The total number of items in the table + */ + count?: number + /** + * Whether to display pagination controls + */ + pagination?: boolean + /** + * Whether the table is empty due to no results from the active query + */ + noResults?: boolean +} + +/** + * TODO + * + * Add a sticky header to the table that shows the column name when scrolling through the table vertically. + * + * This is a bit tricky as we can't support horizontal scrolling and sticky headers at the same time, natively + * with CSS. We need to implement a custom solution for this. One solution is to render a duplicate table header + * using a DIV that, but it will require rerendeing the duplicate header every time the window is resized, to keep + * the columns aligned. + */ + +/** + * Table component for rendering a table with pagination, filtering and ordering. + */ +export const DataTableRoot = ({ + table, + columns, + pagination, + navigateTo, + commands, + count = 0, + noResults = false, +}: DataTableRootProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + const [showStickyBorder, setShowStickyBorder] = useState(false) + + const hasSelect = columns.find((c) => c.id === "select") + const hasActions = columns.find((c) => c.id === "actions") + const hasCommandBar = commands && commands.length > 0 + + const rowSelection = table.getState().rowSelection + const { pageIndex, pageSize } = table.getState().pagination + + const colCount = columns.length - (hasSelect ? 1 : 0) - (hasActions ? 1 : 0) + const colWidth = 100 / colCount + + const handleHorizontalScroll = (e: UIEvent) => { + const scrollLeft = e.currentTarget.scrollLeft + + if (scrollLeft > 0) { + setShowStickyBorder(true) + } else { + setShowStickyBorder(false) + } + } + + return ( +
    +
    + {!noResults ? ( +
+ + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header, index) => { + const isActionHeader = header.id === "actions" + const isSelectHeader = header.id === "select" + const isSpecialHeader = isActionHeader || isSelectHeader + + const firstHeader = headerGroup.headers.findIndex( + (h) => h.id !== "select" + ) + const isFirstHeader = + firstHeader !== -1 + ? header.id === headerGroup.headers[firstHeader].id + : index === 0 + + const isStickyHeader = isSelectHeader || isFirstHeader + + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => { + const to = navigateTo ? navigateTo(row) : undefined + return ( + navigate(to) : undefined} + > + {row.getVisibleCells().map((cell, index) => { + const visibleCells = row.getVisibleCells() + const isSelectCell = cell.id === "select" + + const firstCell = visibleCells.findIndex( + (h) => h.id !== "select" + ) + const isFirstCell = + firstCell !== -1 + ? cell.id === visibleCells[firstCell].id + : index === 0 + + const isStickyCell = isSelectCell || isFirstCell + + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + })} + + ) + })} + +
+ ) : ( +
+ +
+ )} + + {pagination && ( + + )} + {hasCommandBar && ( + + + + {t("general.countSelected", { + count: Object.keys(rowSelection).length, + })} + + + {commands?.map((command, index) => { + return ( + + command.action(rowSelection)} + /> + {index < commands.length - 1 && } + + ) + })} + + + )} + + ) +} + +type PaginationProps = Omit< + ComponentPropsWithoutRef, + "translations" +> + +const Pagination = (props: PaginationProps) => { + const { t } = useTranslation() + + const translations = { + of: t("general.of"), + results: t("general.results"), + pages: t("general.pages"), + prev: t("general.prev"), + next: t("general.next"), + } + + return +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts new file mode 100644 index 0000000000000..8d47458cf9c89 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts @@ -0,0 +1 @@ +export * from "./data-table-root" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx new file mode 100644 index 0000000000000..f1bdb01027dc8 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx @@ -0,0 +1,57 @@ +import { Input } from "@medusajs/ui" +import { ChangeEvent, useCallback, useEffect } from "react" +import { useTranslation } from "react-i18next" + +import { debounce } from "lodash" +import { useSelectedParams } from "../hooks" + +type DataTableSearchProps = { + placeholder?: string + prefix?: string +} + +export const DataTableSearch = ({ + placeholder, + prefix, +}: DataTableSearchProps) => { + const { t } = useTranslation() + const placeholderText = placeholder || t("general.search") + const selectedParams = useSelectedParams({ + param: "q", + prefix, + multiple: false, + }) + + const query = selectedParams.get() + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedOnChange = useCallback( + debounce((e: ChangeEvent) => { + const value = e.target.value + + if (!value) { + selectedParams.delete() + } else { + selectedParams.add(value) + } + }, 500), + [selectedParams] + ) + + useEffect(() => { + return () => { + debouncedOnChange.cancel() + } + }, [debouncedOnChange]) + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts new file mode 100644 index 0000000000000..1f19f481bed4f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts @@ -0,0 +1 @@ +export * from "./data-table-search" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx new file mode 100644 index 0000000000000..56174606a5924 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx @@ -0,0 +1,115 @@ +import { Table, clx } from "@medusajs/ui" +import { ColumnDef } from "@tanstack/react-table" +import { Skeleton } from "../../../common/skeleton" + +type DataTableSkeletonProps = { + columns: ColumnDef[] + rowCount: number + searchable: boolean + orderBy: boolean + filterable: boolean + pagination: boolean +} + +export const DataTableSkeleton = ({ + columns, + rowCount, + filterable, + searchable, + orderBy, + pagination, +}: DataTableSkeletonProps) => { + const rows = Array.from({ length: rowCount }, (_, i) => i) + + const hasToolbar = filterable || searchable || orderBy + const hasSearchOrOrder = searchable || orderBy + + const hasSelect = columns.find((c) => c.id === "select") + const hasActions = columns.find((c) => c.id === "actions") + const colCount = columns.length - (hasSelect ? 1 : 0) - (hasActions ? 1 : 0) + const colWidth = 100 / colCount + + return ( +
+ {hasToolbar && ( +
+ {filterable && } + {hasSearchOrOrder && ( +
+ {searchable && } + {orderBy && } +
+ )} +
+ )} + + + + {columns.map((col, i) => { + const isSelectHeader = col.id === "select" + const isActionsHeader = col.id === "actions" + + const isSpecialHeader = isSelectHeader || isActionsHeader + + return ( + + {isActionsHeader ? null : ( + + )} + + ) + })} + + + + {rows.map((_, j) => ( + + {columns.map((col, k) => { + const isSpecialCell = + col.id === "select" || col.id === "actions" + + return ( + + + + ) + })} + + ))} + +
+ {pagination && ( +
+ +
+ + + +
+
+ )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts new file mode 100644 index 0000000000000..fbed89a8c7c18 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts @@ -0,0 +1 @@ +export * from "./data-table-skeleton" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx new file mode 100644 index 0000000000000..09446bb367a79 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx @@ -0,0 +1,74 @@ +import { memo } from "react" +import { NoRecords } from "../../common/empty-table-content" +import { DataTableQuery, DataTableQueryProps } from "./data-table-query" +import { DataTableRoot, DataTableRootProps } from "./data-table-root" +import { DataTableSkeleton } from "./data-table-skeleton" + +interface DataTableProps + extends DataTableRootProps, + DataTableQueryProps { + isLoading?: boolean + rowCount: number + queryObject?: Record +} + +const MemoizedDataTableRoot = memo(DataTableRoot) as typeof DataTableRoot +const MemoizedDataTableQuery = memo(DataTableQuery) + +export const DataTable = ({ + table, + columns, + pagination, + navigateTo, + commands, + count = 0, + search = false, + orderBy, + filters, + prefix, + queryObject = {}, + rowCount, + isLoading = false, +}: DataTableProps) => { + if (isLoading) { + return ( + + ) + } + + const noQuery = + Object.values(queryObject).filter((v) => Boolean(v)).length === 0 + const noResults = !isLoading && count === 0 && !noQuery + const noRecords = !isLoading && count === 0 && noQuery + + if (noRecords) { + return + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx b/packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx new file mode 100644 index 0000000000000..aeda07245406c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx @@ -0,0 +1,73 @@ +import { useSearchParams } from "react-router-dom" + +export const useSelectedParams = ({ + param, + prefix, + multiple = false, +}: { + param: string + prefix?: string + multiple?: boolean +}) => { + const [searchParams, setSearchParams] = useSearchParams() + const identifier = prefix ? `${prefix}_${param}` : param + const offsetKey = prefix ? `${prefix}_offset` : "offset" + + const add = (value: string) => { + setSearchParams((prev) => { + const newValue = new URLSearchParams(prev) + + const updateMultipleValues = () => { + const existingValues = newValue.get(identifier)?.split(",") || [] + + if (!existingValues.includes(value)) { + existingValues.push(value) + newValue.set(identifier, existingValues.join(",")) + } + } + + const updateSingleValue = () => { + newValue.set(identifier, value) + } + + multiple ? updateMultipleValues() : updateSingleValue() + newValue.delete(offsetKey) + + return newValue + }) + } + + const deleteParam = (value?: string) => { + const deleteMultipleValues = (prev: URLSearchParams) => { + const existingValues = prev.get(identifier)?.split(",") || [] + const index = existingValues.indexOf(value || "") + if (index > -1) { + existingValues.splice(index, 1) + prev.set(identifier, existingValues.join(",")) + } + } + + const deleteSingleValue = (prev: URLSearchParams) => { + prev.delete(identifier) + } + + setSearchParams((prev) => { + if (value) { + multiple ? deleteMultipleValues(prev) : deleteSingleValue(prev) + if (!prev.get(identifier)) { + prev.delete(identifier) + } + } else { + prev.delete(identifier) + } + prev.delete(offsetKey) + return prev + }) + } + + const get = () => { + return searchParams.get(identifier)?.split(",").filter(Boolean) || [] + } + + return { add, delete: deleteParam, get } +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/index.ts new file mode 100644 index 0000000000000..78f00d949d53b --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/index.ts @@ -0,0 +1,2 @@ +export * from "./data-table" +export type { Filter } from "./data-table-filter" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx new file mode 100644 index 0000000000000..44dda62fb4653 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx @@ -0,0 +1,41 @@ +import { Tooltip } from "@medusajs/ui" +import format from "date-fns/format" +import { useTranslation } from "react-i18next" + +type DateCellProps = { + date: Date +} + +export const DateCell = ({ date }: DateCellProps) => { + const value = new Date(date) + value.setMinutes(value.getMinutes() - value.getTimezoneOffset()) + + const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12 + const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM" + + return ( +
+ {`${format( + value, + timestampFormat + )}`} + } + > + {format(value, "dd MMM yyyy")} + +
+ ) +} + +export const DateHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.date")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts new file mode 100644 index 0000000000000..9bf7e52ed0236 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts @@ -0,0 +1 @@ +export * from "./date-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts new file mode 100644 index 0000000000000..8cc5d6026b4fb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts @@ -0,0 +1 @@ +export * from "./status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx new file mode 100644 index 0000000000000..168a94117e69e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx @@ -0,0 +1,32 @@ +import { clx } from "@medusajs/ui" +import { PropsWithChildren } from "react" + +type StatusCellProps = PropsWithChildren<{ + color?: "green" | "red" | "blue" | "orange" | "grey" | "purple" +}> + +export const StatusCell = ({ color, children }: StatusCellProps) => { + return ( +
+
+
+
+ {children} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx new file mode 100644 index 0000000000000..af558b959cec9 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx @@ -0,0 +1,29 @@ +import { Customer } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +export const CustomerCell = ({ customer }: { customer: Customer | null }) => { + if (!customer) { + return - + } + + const { first_name, last_name, email } = customer + const name = [first_name, last_name].filter(Boolean).join(" ") + + return ( +
+
+ {name || email} +
+
+ ) +} + +export const CustomerHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.customer")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts new file mode 100644 index 0000000000000..dbdd97615af02 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts @@ -0,0 +1 @@ +export * from "./customer-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx new file mode 100644 index 0000000000000..e4f2e48aeb6ab --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from "react-i18next" + +export const DisplayIdCell = ({ displayId }: { displayId: number }) => { + return ( +
+ #{displayId} +
+ ) +} + +export const DisplayIdHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.order")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts new file mode 100644 index 0000000000000..057f15de5bdcb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts @@ -0,0 +1 @@ +export * from "./display-id-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx new file mode 100644 index 0000000000000..d899e85468a31 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx @@ -0,0 +1,46 @@ +import type { FulfillmentStatus } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" +import { StatusCell } from "../../common/status-cell" + +type FulfillmentStatusCellProps = { + status: FulfillmentStatus +} + +export const FulfillmentStatusCell = ({ + status, +}: FulfillmentStatusCellProps) => { + const { t } = useTranslation() + + const [label, color] = { + not_fulfilled: [t("orders.fulfillmentStatus.notFulfilled"), "red"], + partially_fulfilled: [ + t("orders.fulfillmentStatus.partiallyFulfilled"), + "orange", + ], + fulfilled: [t("orders.fulfillmentStatus.fulfilled"), "green"], + partially_shipped: [ + t("orders.fulfillmentStatus.partiallyShipped"), + "orange", + ], + shipped: [t("orders.fulfillmentStatus.shipped"), "green"], + partially_returned: [ + t("orders.fulfillmentStatus.partiallyReturned"), + "orange", + ], + returned: [t("orders.fulfillmentStatus.returned"), "green"], + canceled: [t("orders.fulfillmentStatus.canceled"), "red"], + requires_action: [t("orders.fulfillmentStatus.requresAction"), "orange"], + }[status] as [string, "red" | "orange" | "green"] + + return {label} +} + +export const FulfillmentStatusHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.fulfillment")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts new file mode 100644 index 0000000000000..a0f92c11b9f99 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts @@ -0,0 +1 @@ +export * from "./fulfillment-status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts new file mode 100644 index 0000000000000..8df6791eaee23 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts @@ -0,0 +1 @@ +export * from "./items-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx new file mode 100644 index 0000000000000..fc0c3b3403005 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx @@ -0,0 +1,26 @@ +import type { LineItem } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +export const ItemsCell = ({ items }: { items: LineItem[] }) => { + const { t } = useTranslation() + + return ( +
+ + {t("general.items", { + count: items.length, + })} + +
+ ) +} + +export const ItemsHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.items")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts new file mode 100644 index 0000000000000..f6c4b069ee991 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts @@ -0,0 +1 @@ +export * from "./payment-status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx new file mode 100644 index 0000000000000..db8828fa26799 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx @@ -0,0 +1,33 @@ +import type { PaymentStatus } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" +import { StatusCell } from "../../common/status-cell" + +type PaymentStatusCellProps = { + status: PaymentStatus +} + +export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => { + const { t } = useTranslation() + + const [label, color] = { + not_paid: [t("orders.paymentStatus.notPaid"), "red"], + awaiting: [t("orders.paymentStatus.awaiting"), "orange"], + captured: [t("orders.paymentStatus.captured"), "green"], + refunded: [t("orders.paymentStatus.refunded"), "green"], + partially_refunded: [t("orders.paymentStatus.partiallyRefunded"), "orange"], + canceled: [t("orders.paymentStatus.canceled"), "red"], + requires_action: [t("orders.paymentStatus.requresAction"), "orange"], + }[status] as [string, "red" | "orange" | "green"] + + return {label} +} + +export const PaymentStatusHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.payment")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts new file mode 100644 index 0000000000000..8040cf7b1b40e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts @@ -0,0 +1 @@ +export * from "./sales-channel-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx new file mode 100644 index 0000000000000..17e249d0418eb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx @@ -0,0 +1,30 @@ +import { SalesChannel } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +export const SalesChannelCell = ({ + channel, +}: { + channel: SalesChannel | null +}) => { + if (!channel) { + return - + } + + const { name } = channel + + return ( +
+ {name} +
+ ) +} + +export const SalesChannelHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.salesChannel")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts new file mode 100644 index 0000000000000..a58e61c257cee --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts @@ -0,0 +1 @@ +export * from "./total-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx new file mode 100644 index 0000000000000..58646ee9339f4 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next" +import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers" + +type TotalCellProps = { + currencyCode: string + total: number | null +} + +export const TotalCell = ({ currencyCode, total }: TotalCellProps) => { + if (!total) { + return - + } + + const formatted = new Intl.NumberFormat(undefined, { + style: "currency", + currency: currencyCode, + currencyDisplay: "narrowSymbol", + }).format(0) + + const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() + + const presentationAmount = getPresentationalAmount(total, currencyCode) + const formattedTotal = new Intl.NumberFormat(undefined, { + style: "decimal", + }).format(presentationAmount) + + return ( +
+ + {symbol} {formattedTotal} {currencyCode.toUpperCase()} + +
+ ) +} + +export const TotalHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.total")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx new file mode 100644 index 0000000000000..490db287b1497 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx @@ -0,0 +1,133 @@ +import { Order } from "@medusajs/medusa" +import { + ColumnDef, + ColumnDefBase, + createColumnHelper, +} from "@tanstack/react-table" +import { useMemo } from "react" +import { + DateCell, + DateHeader, +} from "../../../components/table/table-cells/common/date-cell" +import { + DisplayIdCell, + DisplayIdHeader, +} from "../../../components/table/table-cells/order/display-id-cell" +import { + FulfillmentStatusCell, + FulfillmentStatusHeader, +} from "../../../components/table/table-cells/order/fulfillment-status-cell" +import { + ItemsCell, + ItemsHeader, +} from "../../../components/table/table-cells/order/items-cell" +import { + PaymentStatusCell, + PaymentStatusHeader, +} from "../../../components/table/table-cells/order/payment-status-cell" +import { + SalesChannelCell, + SalesChannelHeader, +} from "../../../components/table/table-cells/order/sales-channel-cell" +import { + TotalCell, + TotalHeader, +} from "../../../components/table/table-cells/order/total-cell" + +// We have to use any here, as the type of Order is so complex that it lags the TS server +const columnHelper = createColumnHelper() + +type UseOrderTableColumnsProps = { + exclude?: string[] +} + +export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => { + const { exclude = [] } = props ?? {} + + const columns = useMemo( + () => [ + columnHelper.accessor("display_id", { + header: () => , + cell: ({ getValue }) => { + const id = getValue() + + return + }, + }), + columnHelper.accessor("created_at", { + header: () => , + cell: ({ getValue }) => { + const date = new Date(getValue()) + + return + }, + }), + columnHelper.accessor("sales_channel", { + header: () => , + cell: ({ getValue }) => { + const channel = getValue() + + return + }, + }), + columnHelper.accessor("payment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + + return + }, + }), + columnHelper.accessor("fulfillment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + + return + }, + }), + columnHelper.accessor("items", { + header: () => , + cell: ({ getValue }) => { + const items = getValue() + + return + }, + }), + columnHelper.accessor("total", { + header: () => , + cell: ({ getValue, row }) => { + const total = getValue() + const currencyCode = row.original.currency_code + + return + }, + }), + ], + [] + ) + + const isAccessorColumnDef = ( + c: any + ): c is ColumnDef & { accessorKey: string } => { + return c.accessorKey !== undefined + } + + const isDisplayColumnDef = ( + c: any + ): c is ColumnDef & { id: string } => { + return c.id !== undefined + } + + const shouldExclude = >(c: TDef) => { + if (isAccessorColumnDef(c)) { + return exclude.includes(c.accessorKey) + } else if (isDisplayColumnDef(c)) { + return exclude.includes(c.id) + } + + return false + } + + return columns.filter((c) => !shouldExclude(c)) as ColumnDef[] +} diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx new file mode 100644 index 0000000000000..aa5b309f05684 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx @@ -0,0 +1,154 @@ +import { useAdminRegions, useAdminSalesChannels } from "medusa-react" +import { useTranslation } from "react-i18next" + +import type { Filter } from "../../../components/table/data-table" + +export const useOrderTableFilters = (): Filter[] => { + const { t } = useTranslation() + + const { regions } = useAdminRegions({ + limit: 1000, + fields: "id,name", + expand: "", + }) + + const { sales_channels } = useAdminSalesChannels({ + limit: 1000, + fields: "id,name", + expand: "", + }) + + let filters: Filter[] = [] + + if (regions) { + const regionFilter: Filter = { + key: "region_id", + label: t("fields.region"), + type: "select", + options: regions.map((r) => ({ + label: r.name, + value: r.id, + })), + multiple: true, + searchable: true, + } + + filters = [...filters, regionFilter] + } + + if (sales_channels) { + const salesChannelFilter: Filter = { + key: "sales_channel_id", + label: t("fields.salesChannel"), + type: "select", + multiple: true, + searchable: true, + options: sales_channels.map((s) => ({ + label: s.name, + value: s.id, + })), + } + + filters = [...filters, salesChannelFilter] + } + + const paymentStatusFilter: Filter = { + key: "payment_status", + label: t("orders.paymentStatusLabel"), + type: "select", + multiple: true, + options: [ + { + label: t("orders.paymentStatus.notPaid"), + value: "not_paid", + }, + { + label: t("orders.paymentStatus.awaiting"), + value: "awaiting", + }, + { + label: t("orders.paymentStatus.captured"), + value: "captured", + }, + { + label: t("orders.paymentStatus.refunded"), + value: "refunded", + }, + { + label: t("orders.paymentStatus.partiallyRefunded"), + value: "partially_refunded", + }, + { + label: t("orders.paymentStatus.canceled"), + value: "canceled", + }, + { + label: t("orders.paymentStatus.requresAction"), + value: "requires_action", + }, + ], + } + + const fulfillmentStatusFilter: Filter = { + key: "fulfillment_status", + label: t("orders.fulfillmentStatusLabel"), + type: "select", + multiple: true, + options: [ + { + label: t("orders.fulfillmentStatus.notFulfilled"), + value: "not_fulfilled", + }, + { + label: t("orders.fulfillmentStatus.fulfilled"), + value: "fulfilled", + }, + { + label: t("orders.fulfillmentStatus.partiallyFulfilled"), + value: "partially_fulfilled", + }, + { + label: t("orders.fulfillmentStatus.returned"), + value: "returned", + }, + { + label: t("orders.fulfillmentStatus.partiallyReturned"), + value: "partially_returned", + }, + { + label: t("orders.fulfillmentStatus.shipped"), + value: "shipped", + }, + { + label: t("orders.fulfillmentStatus.partiallyShipped"), + value: "partially_shipped", + }, + { + label: t("orders.fulfillmentStatus.canceled"), + value: "canceled", + }, + { + label: t("orders.fulfillmentStatus.requresAction"), + value: "requires_action", + }, + ], + } + + const dateFilters: Filter[] = [ + { label: "Created At", key: "created_at" }, + { label: "Updated At", key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + filters = [ + ...filters, + paymentStatusFilter, + fulfillmentStatusFilter, + ...dateFilters, + ] + + return filters +} diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx new file mode 100644 index 0000000000000..bcf52dad14c1d --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx @@ -0,0 +1,58 @@ +import { AdminGetOrdersParams } from "@medusajs/medusa" +import { useQueryParams } from "../../use-query-params" + +type UseOrderTableQueryProps = { + prefix?: string + pageSize?: number +} + +/** + * TODO: Enable `order` query param when staging is updated + */ + +export const useOrderTableQuery = ({ + prefix, + pageSize = 50, +}: UseOrderTableQueryProps) => { + const queryObject = useQueryParams( + [ + "offset", + "q", + "created_at", + "updated_at", + "region_id", + "sales_channel_id", + "payment_status", + "fulfillment_status", + ], + prefix + ) + + const { + offset, + sales_channel_id, + created_at, + updated_at, + fulfillment_status, + payment_status, + region_id, + q, + } = queryObject + + const searchParams: AdminGetOrdersParams = { + limit: pageSize, + offset: offset ? Number(offset) : 0, + sales_channel_id: sales_channel_id?.split(","), + fulfillment_status: fulfillment_status?.split(","), + payment_status: payment_status?.split(","), + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + region_id: region_id?.split(","), + q, + } + + return { + searchParams, + raw: queryObject, + } +} diff --git a/packages/admin-next/dashboard/src/hooks/use-data-table.tsx b/packages/admin-next/dashboard/src/hooks/use-data-table.tsx new file mode 100644 index 0000000000000..5be85018f4e4c --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-data-table.tsx @@ -0,0 +1,112 @@ +import { + ColumnDef, + OnChangeFn, + PaginationState, + Row, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useEffect, useMemo, useState } from "react" +import { useSearchParams } from "react-router-dom" + +type UseDataTableProps = { + data?: TData[] + columns: ColumnDef[] + count?: number + pageSize?: number + enableRowSelection?: boolean | ((row: Row) => boolean) + enablePagination?: boolean + getRowId?: (original: TData, index: number) => string + prefix?: string +} + +export const useDataTable = ({ + data = [], + columns, + count = 0, + pageSize: _pageSize = 50, + enablePagination = true, + enableRowSelection = false, + getRowId, + prefix, +}: UseDataTableProps) => { + const [searchParams, setSearchParams] = useSearchParams() + const offsetKey = `${prefix ? `${prefix}_` : ""}offset` + const offset = searchParams.get(offsetKey) + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: offset ? Math.ceil(Number(offset) / _pageSize) : 0, + pageSize: _pageSize, + }) + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + const [rowSelection, setRowSelection] = useState({}) + + useEffect(() => { + if (!enablePagination) { + return + } + + const index = offset ? Math.ceil(Number(offset) / _pageSize) : 0 + + if (index === pageIndex) { + return + } + + setPagination((prev) => ({ + ...prev, + pageIndex: index, + })) + }, [offset, enablePagination, _pageSize, pageIndex]) + + const onPaginationChange = ( + updater: (old: PaginationState) => PaginationState + ) => { + const state = updater(pagination) + const { pageIndex, pageSize } = state + + setSearchParams((prev) => { + if (!pageIndex) { + prev.delete(offsetKey) + return prev + } + + const newSearch = new URLSearchParams(prev) + newSearch.set(offsetKey, String(pageIndex * pageSize)) + + return newSearch + }) + + setPagination(state) + return state + } + + const table = useReactTable({ + data, + columns, + state: { + rowSelection, + pagination: enablePagination ? pagination : undefined, + }, + pageCount: Math.ceil((count ?? 0) / pageSize), + enableRowSelection, + getRowId, + onRowSelectionChange: enableRowSelection ? setRowSelection : undefined, + onPaginationChange: enablePagination + ? (onPaginationChange as OnChangeFn) + : undefined, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: enablePagination + ? getPaginationRowModel() + : undefined, + manualPagination: enablePagination ? true : undefined, + }) + + return { table } +} diff --git a/packages/admin-next/dashboard/src/hooks/use-query-params.tsx b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx index afb282362e58b..623975e5fb362 100644 --- a/packages/admin-next/dashboard/src/hooks/use-query-params.tsx +++ b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx @@ -1,15 +1,23 @@ import { useSearchParams } from "react-router-dom" +type QueryParams = { + [key in T]: string | undefined +} + export function useQueryParams( - keys: T[] -): Record { + keys: T[], + prefix?: string +): QueryParams { const [params] = useSearchParams() // Use a type assertion to initialize the result - const result = {} as Record + const result = {} as QueryParams keys.forEach((key) => { - result[key] = params.get(key) || undefined + const prefixedKey = prefix ? `${prefix}_${key}` : key + const value = params.get(prefixedKey) || undefined + + result[key] = value }) return result diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index dbdb2d99dda0b..3cb8603b76591 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -74,7 +74,7 @@ const router = createBrowserRouter([ children: [ { index: true, - lazy: () => import("../../routes/orders/list"), + lazy: () => import("../../routes/orders/order-list"), }, { path: ":id", diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx index 026ad1f84db3c..a2f307c56867b 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -1,99 +1,62 @@ -import { ReceiptPercent } from "@medusajs/icons" -import { Customer, Order } from "@medusajs/medusa" -import { Button, Container, Heading, Table, clx } from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" +import { Customer } from "@medusajs/medusa" +import { Button, Container, Heading } from "@medusajs/ui" import { useAdminOrders } from "medusa-react" -import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" -import { ActionMenu } from "../../../../../components/common/action-menu" -import { NoRecords } from "../../../../../components/common/empty-table-content" -import { - OrderDateCell, - OrderDisplayIdCell, - OrderFulfillmentStatusCell, - OrderPaymentStatusCell, - OrderTotalCell, -} from "../../../../../components/common/order-table-cells" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { DataTable } from "../../../../../components/table/data-table" +import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" +import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" +import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" type CustomerGeneralSectionProps = { customer: Customer } const PAGE_SIZE = 10 +const DEFAULT_RELATIONS = "customer,items,sales_channel" +const DEFAULT_FIELDS = + "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code" export const CustomerOrderSection = ({ customer, }: CustomerGeneralSectionProps) => { const { t } = useTranslation() - const navigate = useNavigate() - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, + const { searchParams, raw } = useOrderTableQuery({ pageSize: PAGE_SIZE, }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - const params = useQueryParams(["q"]) const { orders, count, isLoading, isError, error } = useAdminOrders( { customer_id: customer.id, - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - fields: - "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code", - ...params, + expand: DEFAULT_RELATIONS, + fields: DEFAULT_FIELDS, + ...searchParams, }, { keepPreviousData: true, } ) - const columns = useColumns() + const columns = useOrderTableColumns({ + exclude: ["customer"], + }) + const filters = useOrderTableFilters() - const table = useReactTable({ + const { table } = useDataTable({ data: orders ?? [], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + enablePagination: true, + count, + pageSize: PAGE_SIZE, }) - const noRecords = - Object.values(params).every((v) => !v) && !isLoading && !orders?.length - if (isError) { throw error } return ( - -
+ +
{t("orders.domain")}
- {!noRecords && ( -
-
-
- -
-
- )} - {noRecords ? ( - - ) : ( -
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/orders/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
- -
- )} + `/orders/${row.original.id}`} + filters={filters} + count={count} + isLoading={isLoading} + rowCount={PAGE_SIZE} + orderBy={["display_id", "created_at", "updated_at"]} + search={true} + queryObject={raw} + />
) } - -const OrderActions = ({ order }: { order: Order }) => { - const { t } = useTranslation() - - return ( - , - label: t("customers.viewOrder"), - to: `/orders/${order.id}/edit`, - }, - ], - }, - ]} - /> - ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.accessor("display_id", { - header: "Order", - cell: ({ getValue }) => , - }), - columnHelper.accessor("created_at", { - header: "Date", - cell: ({ getValue }) => , - }), - columnHelper.accessor("fulfillment_status", { - header: "Fulfillment Status", - cell: ({ getValue }) => ( - - ), - }), - columnHelper.accessor("payment_status", { - header: "Payment Status", - cell: ({ getValue }) => , - }), - columnHelper.accessor("total", { - header: () => t("fields.total"), - cell: ({ getValue, row }) => ( - - ), - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => , - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/orders/list/index.ts b/packages/admin-next/dashboard/src/routes/orders/list/index.ts deleted file mode 100644 index ad7ea56183413..0000000000000 --- a/packages/admin-next/dashboard/src/routes/orders/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { OrderList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/orders/list/list.tsx b/packages/admin-next/dashboard/src/routes/orders/list/list.tsx deleted file mode 100644 index d5498116ac06c..0000000000000 --- a/packages/admin-next/dashboard/src/routes/orders/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const OrderList = () => { - return ( -
- - Orders - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts new file mode 100644 index 0000000000000..f6520707347f2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts @@ -0,0 +1 @@ +export * from "./order-list-table" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx new file mode 100644 index 0000000000000..05ba27416a2b2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx @@ -0,0 +1,67 @@ +import { Container, Heading } from "@medusajs/ui" +import { useAdminOrders } from "medusa-react" +import { useTranslation } from "react-i18next" +import { DataTable } from "../../../../../components/table/data-table/data-table" +import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" +import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" +import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" + +const PAGE_SIZE = 50 +const DEFAULT_RELATIONS = "customer,items,sales_channel" +const DEFAULT_FIELDS = + "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code" + +export const OrderListTable = () => { + const { t } = useTranslation() + const { searchParams, raw } = useOrderTableQuery({ + pageSize: PAGE_SIZE, + }) + + const { orders, count, isError, error, isLoading } = useAdminOrders( + { + expand: DEFAULT_RELATIONS, + fields: DEFAULT_FIELDS, + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const filters = useOrderTableFilters() + const columns = useOrderTableColumns({}) + + const { table } = useDataTable({ + data: orders ?? [], + columns, + enablePagination: true, + count, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("orders.domain")} +
+ `/orders/${row.original.id}`} + filters={filters} + count={count} + search + isLoading={isLoading} + rowCount={PAGE_SIZE} + orderBy={["display_id", "created_at", "updated_at"]} + queryObject={raw} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-list/index.ts new file mode 100644 index 0000000000000..0d3535fb846c6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/index.ts @@ -0,0 +1 @@ +export { OrderList as Component } from "./order-list" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx b/packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx new file mode 100644 index 0000000000000..587cb88bf516d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx @@ -0,0 +1,9 @@ +import { OrderListTable } from "./components/order-list-table" + +export const OrderList = () => { + return ( +
+ +
+ ) +} diff --git a/packages/design-system/ui-preset/package.json b/packages/design-system/ui-preset/package.json index 53bb8ebf8f041..22776839f6483 100644 --- a/packages/design-system/ui-preset/package.json +++ b/packages/design-system/ui-preset/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@medusajs/toolbox": "^0.0.1", - "tailwindcss": "^3.3.2", + "tailwindcss": "^3.4.1", "tsup": "^7.1.0", "typescript": "^5.1.6" }, diff --git a/packages/design-system/ui/package.json b/packages/design-system/ui/package.json index d0b0734d22ba1..9653eff581901 100644 --- a/packages/design-system/ui/package.json +++ b/packages/design-system/ui/package.json @@ -72,7 +72,7 @@ "resize-observer-polyfill": "^1.5.1", "rimraf": "^5.0.1", "storybook": "^7.0.23", - "tailwindcss": "^3.3.2", + "tailwindcss": "^3.4.1", "tsc-alias": "^1.8.7", "typescript": "^5.1.6", "vite": "^4.3.9", diff --git a/packages/design-system/ui/src/components/table/table.tsx b/packages/design-system/ui/src/components/table/table.tsx index 246371cbadf77..40f58e18d9028 100644 --- a/packages/design-system/ui/src/components/table/table.tsx +++ b/packages/design-system/ui/src/components/table/table.tsx @@ -49,7 +49,7 @@ const Cell = React.forwardRef< HTMLTableCellElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - + )) Cell.displayName = "Table.Cell" @@ -72,7 +72,11 @@ const HeaderCell = React.forwardRef< HTMLTableCellElement, React.TdHTMLAttributes >(({ className, ...props }, ref) => ( - + )) HeaderCell.displayName = "Table.HeaderCell" diff --git a/yarn.lock b/yarn.lock index bed468a8136a0..a0c934b17d200 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8050,8 +8050,10 @@ __metadata: "@medusajs/ui-preset": "workspace:^" "@medusajs/vite-plugin-extension": "workspace:^" "@radix-ui/react-collapsible": 1.0.3 + "@radix-ui/react-hover-card": ^1.0.7 "@tanstack/react-query": 4.22.0 "@tanstack/react-table": 8.10.7 + "@types/node": ^20.11.15 "@types/react": 18.2.43 "@types/react-dom": 18.2.17 "@uiw/react-json-view": 2.0.0-alpha.10 @@ -8070,7 +8072,7 @@ __metadata: react-hook-form: 7.49.1 react-i18next: 13.5.0 react-router-dom: 6.20.1 - tailwindcss: 3.3.6 + tailwindcss: ^3.4.1 typescript: 5.2.2 vite: 5.0.10 zod: 3.22.4 @@ -8655,7 +8657,7 @@ __metadata: dependencies: "@medusajs/toolbox": ^0.0.1 "@tailwindcss/forms": ^0.5.3 - tailwindcss: ^3.3.2 + tailwindcss: ^3.4.1 tailwindcss-animate: ^1.0.6 tsup: ^7.1.0 typescript: ^5.1.6 @@ -8726,7 +8728,7 @@ __metadata: rimraf: ^5.0.1 storybook: ^7.0.23 tailwind-merge: ^1.13.2 - tailwindcss: ^3.3.2 + tailwindcss: ^3.4.1 tsc-alias: ^1.8.7 typescript: ^5.1.6 vite: ^4.3.9 @@ -10424,6 +10426,34 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-hover-card@npm:^1.0.7": + version: 1.0.7 + resolution: "@radix-ui/react-hover-card@npm:1.0.7" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: f29f3da5bd9a967b5a35e91ac2d1b223191c7a074550d9d9cc10a0c0baf62ba0705b32912a7d2ef1ea5c27dd5e130a9fda9cbe6c2a7f3c2037ed5dfed89aa8cc + languageName: node + linkType: hard + "@radix-ui/react-id@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-id@npm:1.0.0" @@ -17668,6 +17698,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.11.15": + version: 20.11.15 + resolution: "@types/node@npm:20.11.15" + dependencies: + undici-types: ~5.26.4 + checksum: 7dfab4208fedc02e9584c619551906f46ade7955bb929b1e32e354a50522eb532d6bfb2844fdaad2c8dca03be84a590674460c64cb101e1a33bb318e1ec448d4 + languageName: node + linkType: hard + "@types/node@npm:^8.5.7": version: 8.10.66 resolution: "@types/node@npm:8.10.66" @@ -47893,42 +47932,9 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:3.3.6": - version: 3.3.6 - resolution: "tailwindcss@npm:3.3.6" - dependencies: - "@alloc/quick-lru": ^5.2.0 - arg: ^5.0.2 - chokidar: ^3.5.3 - didyoumean: ^1.2.2 - dlv: ^1.1.3 - fast-glob: ^3.3.0 - glob-parent: ^6.0.2 - is-glob: ^4.0.3 - jiti: ^1.19.1 - lilconfig: ^2.1.0 - micromatch: ^4.0.5 - normalize-path: ^3.0.0 - object-hash: ^3.0.0 - picocolors: ^1.0.0 - postcss: ^8.4.23 - postcss-import: ^15.1.0 - postcss-js: ^4.0.1 - postcss-load-config: ^4.0.1 - postcss-nested: ^6.0.1 - postcss-selector-parser: ^6.0.11 - resolve: ^1.22.2 - sucrase: ^3.32.0 - bin: - tailwind: lib/cli.js - tailwindcss: lib/cli.js - checksum: 69caade773249cb963c33e81f85b7fc423dcb74b416727483f434f4e12874187f633970c9de864fa96736289abaf71189314a53589ada0be6c09ccb0e8b78391 - languageName: node - linkType: hard - -"tailwindcss@npm:^3.3.2": - version: 3.3.4 - resolution: "tailwindcss@npm:3.3.4" +"tailwindcss@npm:^3.3.6": + version: 3.4.0 + resolution: "tailwindcss@npm:3.4.0" dependencies: "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 @@ -47955,13 +47961,13 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: a1a0c8c1793b1b1b67503484fe924dc84f79e74c1ddc576095d616eaecc18bbd8fcdbf7c62e07a181673466f4913ebc20d92b93b87da730148b05f7c95e6c83e + checksum: 0a1cef7468e6d17c2857d0b3c4017af2cb37ed8ba27dfb14780c517b8a74f6786970227c400ac1325fc8bcfc09099d8e990fa7c60924bf945f3d0a912d63f546 languageName: node linkType: hard -"tailwindcss@npm:^3.3.6": - version: 3.4.0 - resolution: "tailwindcss@npm:3.4.0" +"tailwindcss@npm:^3.4.1": + version: 3.4.1 + resolution: "tailwindcss@npm:3.4.1" dependencies: "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 @@ -47988,7 +47994,7 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 0a1cef7468e6d17c2857d0b3c4017af2cb37ed8ba27dfb14780c517b8a74f6786970227c400ac1325fc8bcfc09099d8e990fa7c60924bf945f3d0a912d63f546 + checksum: eec3d758f1cd4f51ab3b4c201927c3ecd18e55f8ac94256af60276aaf8d1df78f9dddb5e9fb1e057dfa7cea3c1356add4994cc3d42da9739df874e67047e656f languageName: node linkType: hard