From dc5750dd665a91d35c0246ba83c7f90ec74907f4 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:42:37 +0000 Subject: [PATCH] feat(medusa,types,workflows,utils,product): PricingModule Integration of PriceLists into Core (#5536) --- .changeset/young-items-drop.md | 9 + .../environment-helpers/use-db.js | 14 +- integration-tests/factories/index.ts | 3 +- .../admin/add-price-list-price-batch.spec.ts | 191 +++ .../admin/create-price-list.spec.ts | 191 +++ .../delete-price-list-prices-by-product.ts | 181 +++ .../delete-price-list-prices-by-variant.ts | 132 ++ .../admin/delete-price-list-prices.ts | 146 ++ .../admin/delete-price-list-spec.ts | 145 ++ .../price-lists/admin/get-price-list.spec.ts | 204 +++ .../admin/list-price-list-products.spec.ts | 274 ++++ .../price-lists/admin/list-price-list.spec.ts | 170 +++ .../admin/update-price-list.spec.ts | 226 +++ .../price-lists/store/get-product.ts | 289 ++++ .../plugins/__tests__/pricing/get-product.ts | 2 + .../admin/create-product-variant.spec.ts | 205 +++ .../__tests__/product/admin/create-product.ts | 13 +- .../plugins/__tests__/product/admin/index.ts | 1 + .../admin/update-product-variant.spec.ts | 2 +- .../product/admin/update-product.spec.ts | 7 +- .../helpers/create-default-rule-types.ts | 4 + .../admin/price-lists/add-prices-batch.ts | 62 +- .../admin/price-lists/create-price-list.ts | 87 +- .../admin/price-lists/delete-price-list.ts | 34 +- .../admin/price-lists/delete-prices-batch.ts | 38 +- .../price-lists/delete-product-prices.ts | 44 +- .../delete-products-prices-batch.ts | 44 +- .../price-lists/delete-variant-prices.ts | 45 +- .../admin/price-lists/get-price-list.ts | 21 +- .../src/api/routes/admin/price-lists/index.ts | 45 + .../price-lists/list-price-list-products.ts | 41 +- .../admin/price-lists/list-price-lists.ts | 38 +- .../modules-queries/get-price-list.ts | 29 + .../price-lists/modules-queries/index.ts | 2 + .../list-and-count-price-lists.ts | 104 ++ .../admin/price-lists/update-price-list.ts | 64 +- .../routes/admin/products/create-variant.ts | 91 +- .../api/routes/admin/products/get-product.ts | 6 +- .../routes/admin/products/list-products.ts | 93 +- packages/medusa/src/commands/migrate.js | 12 +- ...198-drop-non-null-constraint-price-list.ts | 20 + packages/medusa/src/models/money-amount.ts | 2 +- .../src/scripts/create-default-rule-types.ts | 25 +- .../medusa/src/scripts/migrate-price-lists.ts | 146 ++ .../money-amount-pricing-module-migration.ts | 2 +- packages/medusa/src/services/pricing.ts | 77 +- packages/medusa/src/types/price-list.ts | 2 +- .../pricing-module/calculate-price.spec.ts | 71 + packages/pricing/src/joiner-config.ts | 25 +- packages/pricing/src/repositories/pricing.ts | 13 +- .../pricing/src/services/pricing-module.ts | 25 +- .../product-module-service/products.spec.ts | 1 + .../src/migrations/Migration20230719100648.ts | 5 +- .../src/services/product-module-service.ts | 76 ++ .../product/src/services/product-variant.ts | 10 +- packages/types/src/product/common.ts | 118 +- packages/types/src/product/service.ts | 1213 +++++++++-------- packages/types/src/workflow/index.ts | 1 + .../workflow/price-list/create-price-list.ts | 78 ++ .../types/src/workflow/price-list/index.ts | 3 + .../workflow/price-list/remove-price-list.ts | 3 + .../workflow/price-list/update-price-list.ts | 25 + .../product/create-product-variants.ts | 32 + packages/types/src/workflow/product/index.ts | 1 + .../src/dal/mikro-orm/mikro-orm-repository.ts | 2 + packages/workflows/src/definition/index.ts | 1 + .../price-list/create-price-lists.ts | 74 + .../src/definition/price-list/index.ts | 5 + .../price-list/remove-price-list-prices.ts | 71 + .../price-list/remove-price-lists.ts | 57 + .../price-list/remove-product-prices.ts | 72 + .../price-list/remove-variant-prices.ts | 72 + .../price-list/update-price-lists.ts | 65 + .../product/create-product-variants.ts | 121 ++ .../workflows/src/definition/product/index.ts | 1 + packages/workflows/src/definitions.ts | 10 + packages/workflows/src/handlers/index.ts | 1 + .../handlers/price-list/create-price-list.ts | 42 + .../src/handlers/price-list/index.ts | 10 + .../price-list/prepare-create-price-list.ts | 87 ++ .../prepare-remove-price-list-prices.ts | 48 + .../prepare-remove-product-prices.ts | 62 + .../prepare-remove-variant-prices.ts | 56 + .../price-list/prepare-update-price-lists.ts | 87 ++ .../handlers/price-list/remove-price-list.ts | 31 + .../remove-price-set-price-list-prices.ts | 39 + .../src/handlers/price-list/remove-prices.ts | 29 + .../handlers/price-list/update-price-lists.ts | 63 + .../create-product-variants-prepare-data.ts | 67 + .../product/create-product-variants.ts | 46 + .../product/create-products-prepare-data.ts | 26 +- .../workflows/src/handlers/product/index.ts | 3 + .../product/remove-product-variants.ts | 26 + .../handlers/product/upsert-variant-prices.ts | 20 +- 94 files changed, 5697 insertions(+), 880 deletions(-) create mode 100644 .changeset/young-items-drop.md create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts create mode 100644 integration-tests/plugins/__tests__/price-lists/store/get-product.ts create mode 100644 integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts create mode 100644 packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts create mode 100644 packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts create mode 100644 packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts create mode 100644 packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts create mode 100644 packages/medusa/src/scripts/migrate-price-lists.ts create mode 100644 packages/types/src/workflow/price-list/create-price-list.ts create mode 100644 packages/types/src/workflow/price-list/index.ts create mode 100644 packages/types/src/workflow/price-list/remove-price-list.ts create mode 100644 packages/types/src/workflow/price-list/update-price-list.ts create mode 100644 packages/types/src/workflow/product/create-product-variants.ts create mode 100644 packages/workflows/src/definition/price-list/create-price-lists.ts create mode 100644 packages/workflows/src/definition/price-list/index.ts create mode 100644 packages/workflows/src/definition/price-list/remove-price-list-prices.ts create mode 100644 packages/workflows/src/definition/price-list/remove-price-lists.ts create mode 100644 packages/workflows/src/definition/price-list/remove-product-prices.ts create mode 100644 packages/workflows/src/definition/price-list/remove-variant-prices.ts create mode 100644 packages/workflows/src/definition/price-list/update-price-lists.ts create mode 100644 packages/workflows/src/definition/product/create-product-variants.ts create mode 100644 packages/workflows/src/handlers/price-list/create-price-list.ts create mode 100644 packages/workflows/src/handlers/price-list/index.ts create mode 100644 packages/workflows/src/handlers/price-list/prepare-create-price-list.ts create mode 100644 packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts create mode 100644 packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts create mode 100644 packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts create mode 100644 packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts create mode 100644 packages/workflows/src/handlers/price-list/remove-price-list.ts create mode 100644 packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts create mode 100644 packages/workflows/src/handlers/price-list/remove-prices.ts create mode 100644 packages/workflows/src/handlers/price-list/update-price-lists.ts create mode 100644 packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts create mode 100644 packages/workflows/src/handlers/product/create-product-variants.ts create mode 100644 packages/workflows/src/handlers/product/remove-product-variants.ts diff --git a/.changeset/young-items-drop.md b/.changeset/young-items-drop.md new file mode 100644 index 0000000000..2e489cfb49 --- /dev/null +++ b/.changeset/young-items-drop.md @@ -0,0 +1,9 @@ +--- +"@medusajs/workflows": patch +"@medusajs/product": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(medusa,types,workflows,utils,product): PricingModule Integration of PriceLists into Core diff --git a/integration-tests/environment-helpers/use-db.js b/integration-tests/environment-helpers/use-db.js index b5ccf47ff0..261ccfac95 100644 --- a/integration-tests/environment-helpers/use-db.js +++ b/integration-tests/environment-helpers/use-db.js @@ -32,6 +32,8 @@ const keepTables = [ "payment_provider", "country", "currency", + "migrations", + "mikro_orm_migrations", ] const DbTestUtil = { @@ -52,21 +54,23 @@ const DbTestUtil = { teardown: async function ({ forceDelete } = {}) { forceDelete = forceDelete || [] - const entities = this.db_.entityMetadatas const manager = this.db_.manager await manager.query(`SET session_replication_role = 'replica';`) + const tableNames = await manager.query(`SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public';`) - for (const entity of entities) { + for (const { table_name } of tableNames) { if ( - keepTables.includes(entity.tableName) && - !forceDelete.includes(entity.tableName) + keepTables.includes(table_name) && + !forceDelete.includes(table_name) ) { continue } await manager.query(`DELETE - FROM "${entity.tableName}";`) + FROM "${table_name}";`) } await manager.query(`SET session_replication_role = 'origin';`) diff --git a/integration-tests/factories/index.ts b/integration-tests/factories/index.ts index 40a248a494..5240f08297 100644 --- a/integration-tests/factories/index.ts +++ b/integration-tests/factories/index.ts @@ -2,6 +2,7 @@ export * from "./simple-batch-job-factory" export * from "./simple-cart-factory" export * from "./simple-custom-shipping-option-factory" export * from "./simple-customer-factory" +export * from "./simple-customer-group-factory" export * from "./simple-discount-factory" export * from "./simple-gift-card-factory" export * from "./simple-line-item-factory" @@ -22,5 +23,5 @@ export * from "./simple-shipping-method-factory" export * from "./simple-shipping-option-factory" export * from "./simple-shipping-profile-factory" export * from "./simple-shipping-tax-rate-factory" -export * from "./simple-tax-rate-factory" export * from "./simple-store-factory" +export * from "./simple-tax-rate-factory" diff --git a/integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts new file mode 100644 index 0000000000..1caaef078d --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts @@ -0,0 +1,191 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/price-lists/:id/prices/batch", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update money amounts if variant id is present in prices", async () => { + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + }, + ]) + + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + prices: [ + { + variant_id: variant.id, + amount: 5000, + currency_code: "usd", + }, + ], + } + + await api.post( + `admin/price-lists/${priceList.id}/prices/batch`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts new file mode 100644 index 0000000000..831251c175 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts @@ -0,0 +1,191 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleCustomerGroupFactory, + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/price-lists", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + await simpleCustomerGroupFactory(dbConnection, { + id: "customer-group-1", + name: "Test Group", + }) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create price list and money amounts", async () => { + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [{ id: "customer-group-1" }], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const result = await api.post(`admin/price-lists`, data, adminHeaders) + + let response = await api.get( + `/admin/price-lists/${result.data.price_list.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: null, + ends_at: null, + customer_groups: [ + { + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "Test Group", + metadata: null, + }, + ], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 400, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts new file mode 100644 index 0000000000..ec8e17fd03 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts @@ -0,0 +1,181 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" +import { AxiosInstance } from "axios" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id/products/:productId/batch", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant1 + let priceSet + let priceListId + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + const api = useApi()! as AxiosInstance + + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant1 = product.variants[0] + + priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant1.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant1.id, + currency_code: "usd", + }, + ], + } + + const priceListResult = await api.post( + `admin/price-lists`, + data, + adminHeaders + ) + priceListId = priceListResult.data.price_list.id + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete prices in batch based on product ids", async () => { + const api = useApi()! as AxiosInstance + + let priceSetMoneyAmounts = + await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + expect(priceSetMoneyAmounts.length).toEqual(2) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/products/prices/batch`, + { + headers: adminHeaders.headers, + data: { + product_ids: [product.id], + }, + } + ) + expect(deleteRes.status).toEqual(200) + + priceSetMoneyAmounts = await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + + expect(priceSetMoneyAmounts.length).toEqual(1) + expect(priceSetMoneyAmounts).toEqual([ + expect.objectContaining({ + price_list: null, + }), + ]) + }) + + it("should delete prices based on single product id", async () => { + const api = useApi()! as AxiosInstance + + let priceSetMoneyAmounts = + await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + expect(priceSetMoneyAmounts.length).toEqual(2) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/products/${product.id}/prices`, + adminHeaders + ) + expect(deleteRes.status).toEqual(200) + + priceSetMoneyAmounts = await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + + expect(priceSetMoneyAmounts.length).toEqual(1) + expect(priceSetMoneyAmounts).toEqual([ + expect.objectContaining({ + price_list: null, + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts new file mode 100644 index 0000000000..cc0639a075 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts @@ -0,0 +1,132 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id/variants/:variantId/prices", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete all prices based on product variant ids", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const result = await api.post(`admin/price-lists`, data, adminHeaders) + const priceListId = result.data.price_list.id + + let psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(1) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/variants/${variant.id}/prices`, + adminHeaders + ) + expect(deleteRes.status).toEqual(200) + + psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(0) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts new file mode 100644 index 0000000000..9924193e3e --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts @@ -0,0 +1,146 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete price list prices by money amount ids", async () => { + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + { + amount: 4000, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const res = await api.post(`admin/price-lists`, data, adminHeaders) + + const priceListId = res.data.price_list.id + let psmas = await pricingModuleService.listPriceSetMoneyAmounts( + { + price_list_id: [priceListId], + }, + { relations: ["money_amount"] } + ) + + expect(psmas.length).toEqual(2) + + const deletePrice = psmas[0].money_amount + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/prices/batch`, + { + data: { + price_ids: [deletePrice?.id], + }, + ...adminHeaders, + } + ) + expect(deleteRes.status).toEqual(200) + + psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(1) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts new file mode 100644 index 0000000000..47064fed15 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts @@ -0,0 +1,145 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete price list and money amounts", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const result = await api.post(`admin/price-lists`, data, adminHeaders) + const priceListId = result.data.price_list.id + + const getResponse = await api.get( + `/admin/price-lists/${priceListId}`, + adminHeaders + ) + expect(getResponse.status).toEqual(200) + + let psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(1) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}`, + adminHeaders + ) + expect(deleteRes.status).toEqual(200) + + const afterDelete = await api + .get(`/admin/price-lists/${priceListId}`, adminHeaders) + .catch((err) => { + return err + }) + expect(afterDelete.response.status).toEqual(404) + + psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(0) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts new file mode 100644 index 0000000000..9eed1dd607 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts @@ -0,0 +1,204 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { simpleProductFactory } from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get price list and its money amounts with variants", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + await pricingModuleService.createPriceLists([ + { + title: "test price list 1", + description: "test 1", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + const response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }) + ) + }) + + it("should throw an error when price list is not found", async () => { + const api = useApi() as any + + const error = await api + .get(`/admin/price-lists/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(error.response.status).toBe(404) + expect(error.response.data).toEqual({ + type: "not_found", + message: "Price list with id: does-not-exist was not found", + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts new file mode 100644 index 0000000000..1395fa75d8 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts @@ -0,0 +1,274 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { simpleProductFactory } from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /admin/price-lists/:id/products", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let product2 + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + title: "uniquely fun product", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + + product2 = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant-2", + title: "uniquely fun product 2", + variants: [ + { + options: [{ option_id: "test-product-option-2", value: "test 2" }], + }, + ], + options: [ + { + id: "test-product-option-2", + title: "Test option 2", + }, + ], + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should list all products in a price list", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + let response = await api.get( + `/admin/price-lists/${priceList.id}/products`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + title: expect.any(String), + handle: expect.any(String), + subtitle: null, + description: null, + is_giftcard: false, + status: "draft", + thumbnail: null, + weight: null, + length: null, + height: null, + width: null, + origin_country: null, + hs_code: null, + mid_code: null, + material: null, + collection_id: null, + collection: null, + type_id: null, + type: null, + discountable: true, + external_id: null, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + metadata: null, + }), + ]) + + response = await api.get( + `/admin/products?price_list_id[]=${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + title: expect.any(String), + handle: expect.any(String), + subtitle: null, + description: null, + is_giftcard: false, + status: "draft", + thumbnail: null, + weight: null, + length: null, + height: null, + width: null, + origin_country: null, + hs_code: null, + mid_code: null, + material: null, + collection_id: null, + collection: null, + type_id: null, + type: null, + discountable: true, + external_id: null, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + metadata: null, + }), + ]) + }) + + it("should list all products constrained by search query in a price list", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + let response = await api.get( + `/admin/price-lists/${priceList.id}/products?q=shouldnotreturnanything`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(0) + expect(response.data.products).toEqual([]) + + response = await api.get( + `/admin/price-lists/${priceList.id}/products?q=uniquely`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + }), + ]) + + response = await api.get( + `/admin/price-lists/${priceList.id}/products?q=`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts new file mode 100644 index 0000000000..fbe8a9830f --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts @@ -0,0 +1,170 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { simpleProductFactory } from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /admin/price-lists", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get price list and its money amounts with variants", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + const response = await api.get(`/admin/price-lists`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.price_lists).toEqual([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts new file mode 100644 index 0000000000..20dfecf877 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts @@ -0,0 +1,226 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleCustomerGroupFactory, + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let variant2 + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + await simpleCustomerGroupFactory(dbConnection, { + id: "customer-group-2", + name: "Test Group 2", + }) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + { + options: [{ option_id: "test-product-option-2", value: "test 2" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + { + id: "test-product-option-2", + title: "Test option 2", + }, + ], + }) + + variant = product.variants[0] + variant2 = product.variants[1] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update price lists successfully with prices", async () => { + const var2PriceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant2.id, + prices: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 3000, + currency_code: "usd", + price_set_id: var2PriceSet.id, + }, + ], + }, + ]) + + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "new price list name", + description: "new price list description", + customer_groups: [{ id: "customer-group-2" }], + prices: [ + { + variant_id: variant.id, + amount: 5000, + currency_code: "usd", + }, + { + id: priceList?.price_set_money_amounts?.[0].money_amount?.id, + amount: 6000, + currency_code: "usd", + variant_id: variant2.id, + }, + ], + } + + await api.post(`admin/price-lists/${priceList.id}`, data, adminHeaders) + + const response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "new price list name", + description: "new price list description", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [ + { + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "Test Group 2", + metadata: null, + }, + ], + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: priceList.id, + region_id: null, + variant: expect.objectContaining({ + id: variant.id, + }), + variant_id: variant.id, + }), + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 6000, + min_quantity: null, + max_quantity: null, + price_list_id: priceList.id, + region_id: null, + variant: expect.objectContaining({ + id: variant2.id, + }), + variant_id: variant2.id, + }), + ]), + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/store/get-product.ts b/integration-tests/plugins/__tests__/price-lists/store/get-product.ts new file mode 100644 index 0000000000..ad5915f910 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/store/get-product.ts @@ -0,0 +1,289 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleCustomerFactory, + simpleCustomerGroupFactory, + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +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 { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /store/products/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let priceSetId + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + status: "published", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + { + amount: 4000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + priceSetId = priceSet.id + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get product and its prices from price-list created through the price list workflow", async () => { + const api = useApi()! as AxiosInstance + + const priceListResponse = await api.post( + `/admin/price-lists`, + { + name: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 2500, + currency_code: "usd", + variant_id: variant.id, + }, + ], + }, + adminHeaders + ) + + let response = await api.get( + `/store/products/${product.id}?currency_code=usd` + ) + + expect(response.status).toEqual(200) + expect(response.data.product.variants[0].prices).toHaveLength(2) + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + currency_code: "usd", + amount: 3000, + min_quantity: null, + max_quantity: null, + price_list_id: null, + }), + expect.objectContaining({ + currency_code: "usd", + amount: 2500, + min_quantity: null, + max_quantity: null, + price_list_id: priceListResponse.data.price_list.id, + }), + ]) + expect(response.data.product.variants[0]).toEqual( + expect.objectContaining({ + original_price: 3000, + calculated_price: 2500, + calculated_price_type: "sale", + }) + ) + }) + + it("should not list prices from price-list with customer groups if not logged in", async () => { + const api = useApi()! as AxiosInstance + + const { id: customerGroupId } = await simpleCustomerGroupFactory( + dbConnection + ) + + const priceListResponse = await api.post( + `/admin/price-lists`, + { + name: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 2500, + currency_code: "usd", + variant_id: variant.id, + }, + ], + customer_groups: [{ id: customerGroupId }], + }, + adminHeaders + ) + + let response = await api.get( + `/store/products/${product.id}?currency_code=usd` + ) + + expect(response.status).toEqual(200) + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + currency_code: "usd", + amount: 3000, + min_quantity: null, + max_quantity: null, + price_list_id: null, + }), + ]) + expect(response.data.product.variants[0]).toEqual( + expect.objectContaining({ + original_price: 3000, + calculated_price: 3000, + calculated_price_type: null, + }) + ) + }) + + it("should list prices from price-list with customer groups", async () => { + const api = useApi()! as AxiosInstance + + await simpleCustomerFactory(dbConnection, { + id: "test-customer-5-pl", + email: "test5@email-pl.com", + first_name: "John", + last_name: "Deere", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + groups: [{ id: "customer-group-1" }], + }) + + const authResponse = await api.post("/store/auth", { + email: "test5@email-pl.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const priceListResponse = await api.post( + `/admin/price-lists`, + { + name: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 2500, + currency_code: "usd", + variant_id: variant.id, + }, + ], + customer_groups: [{ id: "customer-group-1" }], + }, + adminHeaders + ) + + let response = await api.get( + `/store/products/${product.id}?currency_code=usd`, + { + headers: { + Cookie: authCookie, + }, + } + ) + + expect(response.status).toEqual(200) + expect(response.data.product.variants[0].prices).toHaveLength(2) + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + currency_code: "usd", + amount: 3000, + min_quantity: null, + max_quantity: null, + price_list_id: null, + }), + expect.objectContaining({ + currency_code: "usd", + amount: 2500, + min_quantity: null, + max_quantity: null, + price_list_id: priceListResponse.data.price_list.id, + }), + ]) + expect(response.data.product.variants[0]).toEqual( + expect.objectContaining({ + original_price: 3000, + calculated_price: 2500, + calculated_price_type: "sale", + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/pricing/get-product.ts b/integration-tests/plugins/__tests__/pricing/get-product.ts index 0e7a959128..2d68c82f4d 100644 --- a/integration-tests/plugins/__tests__/pricing/get-product.ts +++ b/integration-tests/plugins/__tests__/pricing/get-product.ts @@ -8,6 +8,7 @@ import path from "path" import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app" import { getContainer } from "../../../environment-helpers/use-container" import adminSeeder from "../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../helpers/create-default-rule-types" jest.setTimeout(5000000) @@ -46,6 +47,7 @@ describe("Link Modules", () => { }) beforeEach(async () => { + await createDefaultRuleTypes(medusaContainer) await adminSeeder(dbConnection) await simpleRegionFactory(dbConnection, { id: "region-1", 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 new file mode 100644 index 0000000000..b4482f482a --- /dev/null +++ b/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts @@ -0,0 +1,205 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +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" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/products/:id/variants", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create a product variant with its price sets and prices through the workflow", async () => { + const api = useApi()! as AxiosInstance + const data = { + title: "test variant create", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + region_id: null, + }, + ], + material: "boo", + mid_code: "234asdfadsf", + hs_code: "asdfasdf234", + origin_country: "DE", + sku: "asdf", + ean: "234", + upc: "234", + barcode: "asdf", + inventory_quantity: 234, + manage_inventory: true, + allow_backorder: true, + weight: 234, + width: 234, + height: 234, + length: 234, + metadata: { asdf: "asdf" }, + options: [{ option_id: "test-product-option-1", value: "test option" }], + } + + let response = await api.post( + `/admin/products/${product.id}/variants`, + data, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + title: "test variant create", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + amount: 55500, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) + + it("should compensate creating product variants when error throws in future step", async () => { + jest + .spyOn(PricingModuleService.prototype, "create") + .mockImplementation(() => { + throw new Error("Random Error") + }) + + const productSpy = jest.spyOn( + ProductModuleService.prototype, + "deleteVariants" + ) + + const api = useApi()! as AxiosInstance + const data = { + title: "test variant create", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + region_id: null, + }, + ], + material: "boo", + mid_code: "234asdfadsf", + hs_code: "asdfasdf234", + origin_country: "DE", + sku: "asdf", + ean: "234", + upc: "234", + barcode: "asdf", + inventory_quantity: 234, + manage_inventory: true, + allow_backorder: true, + weight: 234, + width: 234, + height: 234, + length: 234, + metadata: { asdf: "asdf" }, + options: [{ option_id: "test-product-option-1", value: "test option" }], + } + + await api + .post(`/admin/products/${product.id}/variants`, data, adminHeaders) + .catch((e) => e) + + expect(productSpy).toBeCalledWith([expect.any(String)]) + + const getProductResponse = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + expect(getProductResponse.data.product.variants).toHaveLength(1) + }) +}) diff --git a/integration-tests/plugins/__tests__/product/admin/create-product.ts b/integration-tests/plugins/__tests__/product/admin/create-product.ts index 7471d5ee45..a286b03e77 100644 --- a/integration-tests/plugins/__tests__/product/admin/create-product.ts +++ b/integration-tests/plugins/__tests__/product/admin/create-product.ts @@ -1,11 +1,13 @@ import { initDb, useDb } from "../../../../environment-helpers/use-db" import { Region } from "@medusajs/medusa" +import { IPricingModuleService } from "@medusajs/types" import { AxiosInstance } from "axios" 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 { simpleSalesChannelFactory } from "../../../../factories" import adminSeeder from "../../../../helpers/admin-seeder" import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" @@ -21,7 +23,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true, } -describe("[Product & Pricing Module] POST /admin/products", () => { +describe("POST /admin/products", () => { let dbConnection let appContainer let shutdownServer @@ -50,6 +52,8 @@ describe("[Product & Pricing Module] POST /admin/products", () => { currency_code: "usd", tax_rate: 0, }) + + await simpleSalesChannelFactory(dbConnection, { is_default: true }) }) afterEach(async () => { @@ -110,5 +114,12 @@ describe("[Product & Pricing Module] POST /admin/products", () => { ]), }), }) + + const pricingModuleService: IPricingModuleService = appContainer.resolve( + "pricingModuleService" + ) + + const [_, count] = await pricingModuleService.listAndCount() + expect(count).toEqual(1) }) }) diff --git a/integration-tests/plugins/__tests__/product/admin/index.ts b/integration-tests/plugins/__tests__/product/admin/index.ts index c46ace8b44..3d388e03ab 100644 --- a/integration-tests/plugins/__tests__/product/admin/index.ts +++ b/integration-tests/plugins/__tests__/product/admin/index.ts @@ -441,6 +441,7 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + await createDefaultRuleTypes(medusaContainer) await simpleSalesChannelFactory(dbConnection, { name: "Default channel", diff --git a/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts index 590d340457..a7cb0ebaac 100644 --- a/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts @@ -25,7 +25,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true, } -describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", () => { +describe("POST /admin/products/:id/variants/:id", () => { let dbConnection let appContainer let shutdownServer diff --git a/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts b/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts index b631eb0a15..db9750f264 100644 --- a/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts @@ -23,7 +23,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true, } -describe("[Product & Pricing Module] POST /admin/products/:id", () => { +describe("POST /admin/products/:id", () => { let dbConnection let appContainer let shutdownServer @@ -109,10 +109,11 @@ describe("[Product & Pricing Module] POST /admin/products/:id", () => { ) expect(response.status).toEqual(200) + expect(response.data.product.variants).toHaveLength(1) expect(response.data.product).toEqual( expect.objectContaining({ id: expect.any(String), - variants: expect.arrayContaining([ + variants: [ expect.objectContaining({ id: variant.id, title: "test variant update", @@ -128,7 +129,7 @@ describe("[Product & Pricing Module] POST /admin/products/:id", () => { }), ]), }), - ]), + ], }) ) }) diff --git a/integration-tests/plugins/helpers/create-default-rule-types.ts b/integration-tests/plugins/helpers/create-default-rule-types.ts index e063d642bb..6db9fb25ef 100644 --- a/integration-tests/plugins/helpers/create-default-rule-types.ts +++ b/integration-tests/plugins/helpers/create-default-rule-types.ts @@ -10,5 +10,9 @@ export const createDefaultRuleTypes = async (container) => { name: "region_id", rule_attribute: "region_id", }, + { + name: "customer_group_id", + rule_attribute: "customer_group_id", + }, ]) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts b/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts index 64bd889064..7e2b51109d 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts @@ -1,12 +1,15 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { updatePriceLists } from "@medusajs/workflows" +import { Type } from "class-transformer" import { IsArray, IsBoolean, IsOptional, ValidateNested } from "class-validator" +import { EntityManager } from "typeorm" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." - -import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" import { PriceList } from "../../../.." import PriceListService from "../../../../services/price-list" -import { Type } from "class-transformer" +import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" import { validator } from "../../../../utils/validator" -import { EntityManager } from "typeorm" +import { MedusaContainer } from "@medusajs/types" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [post] /admin/price-lists/{id}/prices/batch @@ -85,23 +88,48 @@ import { EntityManager } from "typeorm" */ export default async (req, res) => { const { id } = req.params - - const validated = await validator(AdminPostPriceListPricesPricesReq, req.body) - + let priceList + const featureFlagRouter = req.scope.resolve("featureFlagRouter") + const manager: EntityManager = req.scope.resolve("manager") const priceListService: PriceListService = req.scope.resolve("priceListService") - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .addPrices(id, validated.prices, validated.override) - }) + const validated = await validator(AdminPostPriceListPricesPricesReq, req.body) - const priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const updatePriceListWorkflow = updatePriceLists(req.scope) + + const input = { + price_lists: [ + { + id, + ...validated, + }, + ], + } + + await updatePriceListWorkflow.run({ + input, + context: { + manager, + }, + }) + + priceList = await getPriceListPricingModule(id, { + container: req.scope as MedusaContainer, + }) + } else { + await manager.transaction(async (transactionManager) => { + await priceListService + .withTransaction(transactionManager) + .addPrices(id, validated.prices, validated.override) + }) + + priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts index fc9fc6e712..1d1cbaf1d9 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts @@ -1,3 +1,12 @@ +import { MedusaContainer, PricingTypes, WorkflowTypes } from "@medusajs/types" +import { + FlagRouter, + MedusaV2Flag, + PriceListStatus, + PriceListType, +} from "@medusajs/utils" +import { createPriceLists } from "@medusajs/workflows" +import { Type } from "class-transformer" import { IsArray, IsBoolean, @@ -6,23 +15,18 @@ import { IsString, ValidateNested, } from "class-validator" +import { Request } from "express" +import { EntityManager } from "typeorm" +import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." +import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing" +import { PriceList } from "../../../../models" +import PriceListService from "../../../../services/price-list" import { AdminPriceListPricesCreateReq, CreatePriceListInput, } from "../../../../types/price-list" - -import { PriceListStatus, PriceListType } from "@medusajs/utils" -import { Type } from "class-transformer" -import { Request } from "express" -import { EntityManager } from "typeorm" -import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing" -import { PriceList } from "../../../../models" -import PriceListService from "../../../../services/price-list" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" -import { - defaultAdminPriceListFields, - defaultAdminPriceListRelations, -} from "./index" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [post] /admin/price-lists @@ -109,16 +113,57 @@ export default async (req: Request, res) => { req.scope.resolve("priceListService") const manager: EntityManager = req.scope.resolve("manager") - let priceList = await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .create(req.validatedBody as CreatePriceListInput) - }) + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + let priceList - priceList = await priceListService.retrieve(priceList.id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key + ) + + if (isMedusaV2FlagEnabled) { + const createPriceListWorkflow = createPriceLists(req.scope) + const validatedInput = req.validatedBody as CreatePriceListInput + const rules: PricingTypes.CreatePriceListRules = {} + const customerGroups = validatedInput?.customer_groups || [] + delete validatedInput.customer_groups + + if (customerGroups.length) { + rules["customer_group_id"] = customerGroups.map((cg) => cg.id) + } + + const input = { + price_lists: [ + { + ...validatedInput, + rules, + }, + ], + } as WorkflowTypes.PriceListWorkflow.CreatePriceListWorkflowInputDTO + + const { result } = await createPriceListWorkflow.run({ + input, + context: { + manager, + }, + }) + + priceList = result[0]!.priceList + + priceList = await getPriceListPricingModule(priceList.id, { + container: req.scope as MedusaContainer, + }) + } else { + priceList = await manager.transaction(async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .create(req.validatedBody as CreatePriceListInput) + }) + + priceList = await priceListService.retrieve(priceList.id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts index 54af5cfaaf..94db28f0d6 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts @@ -1,3 +1,6 @@ +import { WorkflowTypes } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceLists } from "@medusajs/workflows" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" @@ -56,12 +59,33 @@ import PriceListService from "../../../../services/price-list" export default async (req, res) => { const { id } = req.params - const priceListService: PriceListService = - req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService.withTransaction(transactionManager).delete(id) - }) + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key + ) + + if (isMedusaV2FlagEnabled) { + const removePriceListsWorkflow = removePriceLists(req.scope) + + const input = { + price_lists: [id], + } as WorkflowTypes.PriceListWorkflow.RemovePriceListWorkflowInputDTO + + await removePriceListsWorkflow.run({ + input, + context: { + manager, + }, + }) + } else { + const priceListService: PriceListService = + req.scope.resolve("priceListService") + await manager.transaction(async (transactionManager) => { + await priceListService.withTransaction(transactionManager).delete(id) + }) + } res.json({ id, diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts index 5de013889f..96fa6f8d09 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts @@ -1,8 +1,10 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListProductPrices } from "@medusajs/workflows/dist/definition/price-list/remove-price-list-prices" import { ArrayNotEmpty, IsString } from "class-validator" - import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" import { validator } from "../../../../utils/validator" +import { WorkflowTypes } from "@medusajs/types" /** * @oas [delete] /admin/price-lists/{id}/prices/batch @@ -81,13 +83,37 @@ export default async (req, res) => { const priceListService: PriceListService = req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deletePrices(id, validated.price_ids) - }) + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key + ) + + if (isMedusaV2FlagEnabled) { + const deletePriceListPricesWorkflow = removePriceListProductPrices( + req.scope + ) + + const input = { + price_list_id: id, + money_amount_ids: validated.price_ids, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListPricesWorkflowInputDTO + + await deletePriceListPricesWorkflow.run({ + input, + context: { + manager, + }, + }) + } else { + await manager.transaction(async (transactionManager) => { + await priceListService + .withTransaction(transactionManager) + .deletePrices(id, validated.price_ids) + }) + } res.json({ ids: validated.price_ids, object: "money-amount", deleted: true }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts index 6c424c9624..d12de2e6e4 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts @@ -1,3 +1,6 @@ +import { WorkflowTypes } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListProductPrices } from "@medusajs/workflows/dist/definition/price-list/remove-product-prices" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" @@ -60,15 +63,44 @@ export default async (req, res) => { const priceListService: PriceListService = req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - const [deletedPriceIds] = await manager.transaction( - async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deleteProductPrices(id, [product_id]) - } + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key ) + let deletedPriceIds: string[] = [] + + if (isMedusaV2FlagEnabled) { + const deletePriceListProductsWorkflow = removePriceListProductPrices( + req.scope + ) + + const input = { + product_ids: [product_id], + price_list_id: id, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO + + const { result } = await deletePriceListProductsWorkflow.run({ + input, + context: { + manager, + }, + }) + + deletedPriceIds = result + } else { + const [deletedIds] = await manager.transaction( + async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .deleteProductPrices(id, [product_id]) + } + ) + deletedPriceIds = deletedIds + } + return res.json({ ids: deletedPriceIds, object: "money-amount", diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts index 4209879087..129a21ccfc 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts @@ -1,8 +1,11 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListProductPrices } from "@medusajs/workflows" import { ArrayNotEmpty, IsString } from "class-validator" import { Request, Response } from "express" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" import { validator } from "../../../../utils/validator" +import { WorkflowTypes } from "@medusajs/types" /** * @oas [delete] /admin/price-lists/{id}/products/prices/batch @@ -79,15 +82,44 @@ export default async (req: Request, res: Response) => { const priceListService: PriceListService = req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - const [deletedPriceIds] = await manager.transaction( - async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deleteProductPrices(id, validated.product_ids) - } + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key ) + let deletedPriceIds: string[] = [] + + if (isMedusaV2FlagEnabled) { + const deletePriceListProductsWorkflow = removePriceListProductPrices( + req.scope + ) + + const input = { + product_ids: validated.product_ids, + price_list_id: id, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO + + const { result } = await deletePriceListProductsWorkflow.run({ + input, + context: { + manager, + }, + }) + deletedPriceIds = result + } else { + const [deletedIds] = await manager.transaction( + async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .deleteProductPrices(id, validated.product_ids) + } + ) + + deletedPriceIds = deletedIds + } + return res.json({ ids: deletedPriceIds, object: "money-amount", diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts index 4ba20c9750..45a61d643f 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts @@ -1,5 +1,8 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListVariantPrices } from "@medusajs/workflows" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" +import { WorkflowTypes } from "@medusajs/types" /** * @oas [delete] /admin/price-lists/{id}/variants/{variant_id}/prices @@ -59,16 +62,44 @@ export default async (req, res) => { const priceListService: PriceListService = req.scope.resolve("priceListService") - + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - const [deletedPriceIds] = await manager.transaction( - async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deleteVariantPrices(id, [variant_id]) - } + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key ) + let deletedPriceIds: string[] = [] + + if (isMedusaV2FlagEnabled) { + const deletePriceListProductsWorkflow = removePriceListVariantPrices( + req.scope + ) + + const input = { + variant_ids: [variant_id], + price_list_id: id, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListVariantsWorkflowInputDTO + + const { result } = await deletePriceListProductsWorkflow.run({ + input, + context: { + manager, + }, + }) + deletedPriceIds = result + } else { + const [deletedIds] = await manager.transaction( + async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .deleteVariantPrices(id, [variant_id]) + } + ) + + deletedPriceIds = deletedIds + } + return res.json({ ids: deletedPriceIds, object: "money-amount", diff --git a/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts index 0939d06b0d..9faa088f55 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts @@ -1,7 +1,9 @@ +import { MedusaContainer } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." - import { PriceList } from "../../../.." import PriceListService from "../../../../services/price-list" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [get] /admin/price-lists/{id} @@ -58,13 +60,22 @@ import PriceListService from "../../../../services/price-list" export default async (req, res) => { const { id } = req.params + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const priceListService: PriceListService = req.scope.resolve("priceListService") - const priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + let priceList + + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + priceList = await getPriceListPricingModule(id, { + container: req.scope as MedusaContainer, + }) + } else { + priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.status(200).json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/index.ts b/packages/medusa/src/api/routes/admin/price-lists/index.ts index f4ff52921a..bc324b09a7 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/index.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/index.ts @@ -9,6 +9,7 @@ import middlewares, { import { defaultAdminProductFields, defaultAdminProductRelations, + defaultAdminProductRemoteQueryObject, } from "../products" import { FlagRouter } from "@medusajs/utils" @@ -86,6 +87,50 @@ export default (app, featureFlagRouter: FlagRouter) => { return app } +export const defaultAdminPriceListRemoteQueryObject = { + fields: [ + "created_at", + "deleted_at", + "description", + "ends_at", + "id", + "title", + "starts_at", + "status", + "type", + "updated_at", + ], + price_list_rules: { + price_list_rule_values: { + fields: ["value"], + }, + rule_type: { + fields: ["rule_attribute"], + }, + }, + price_set_money_amounts: { + money_amount: { + fields: [ + "id", + "currency_code", + "amount", + "min_quantity", + "max_quantity", + "created_at", + "deleted_at", + "updated_at", + ], + }, + price_set: { + variant_link: { + variant: { + fields: defaultAdminProductRemoteQueryObject.variants.fields, + }, + }, + }, + }, +} + export const defaultAdminPriceListFields = [ "id", "name", diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 6fc229b553..ec36d3838e 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -1,7 +1,3 @@ -import { - DateComparisonOperator, - extendedFindParamsMixin, -} from "../../../../types/common" import { IsArray, IsBoolean, @@ -10,14 +6,20 @@ import { IsString, ValidateNested, } from "class-validator" -import { MedusaError, isDefined } from "medusa-core-utils" +import { isDefined } from "medusa-core-utils" +import { + DateComparisonOperator, + extendedFindParamsMixin, +} from "../../../../types/common" -import { FilterableProductProps } from "../../../../types/product" -import PriceListService from "../../../../services/price-list" -import { ProductStatus } from "../../../../models" -import { Request } from "express" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { Type } from "class-transformer" +import { Request } from "express" import { pickBy } from "lodash" +import { ProductStatus } from "../../../../models" +import PriceListService from "../../../../services/price-list" +import { FilterableProductProps } from "../../../../types/product" +import { listAndCountProductWithIsolatedProductModule } from "../products/list-products" /** * @oas [get] /admin/price-lists/{id}/products @@ -181,6 +183,9 @@ import { pickBy } from "lodash" export default async (req: Request, res) => { const { id } = req.params const { offset, limit } = req.validatedQuery + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + let products + let count const priceListService: PriceListService = req.scope.resolve("priceListService") @@ -190,11 +195,19 @@ export default async (req: Request, res) => { price_list_id: [id], } - const [products, count] = await priceListService.listProducts( - id, - pickBy(filterableFields, (val) => isDefined(val)), - req.listConfig - ) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + ;[products, count] = await listAndCountProductWithIsolatedProductModule( + req, + filterableFields, + req.listConfig + ) + } else { + ;[products, count] = await priceListService.listProducts( + id, + pickBy(filterableFields, (val) => isDefined(val)), + req.listConfig + ) + } res.json({ products, diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts index d868794fb8..b1d3013cc3 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts @@ -1,9 +1,11 @@ -import { IsNumber, IsOptional, IsString } from "class-validator" - -import { FilterablePriceListProps } from "../../../../types/price-list" -import PriceListService from "../../../../services/price-list" -import { Request } from "express" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { Type } from "class-transformer" +import { IsNumber, IsOptional, IsString } from "class-validator" +import { Request } from "express" +import PriceListService from "../../../../services/price-list" +import { FilterablePriceListProps } from "../../../../types/price-list" +import { MedusaContainer } from "@medusajs/types" +import { listAndCountPriceListPricingModule } from "./modules-queries" /** * @oas [get] /admin/price-lists @@ -161,18 +163,30 @@ import { Type } from "class-transformer" * $ref: "#/components/responses/500_error" */ export default async (req: Request, res) => { + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + const validated = req.validatedQuery + let priceLists + let count - const priceListService: PriceListService = - req.scope.resolve("priceListService") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + ;[priceLists, count] = await listAndCountPriceListPricingModule({ + filters: req.filterableFields, + listConfig: req.listConfig, + container: req.scope as MedusaContainer, + }) + } else { + const priceListService: PriceListService = + req.scope.resolve("priceListService") - const [price_lists, count] = await priceListService.listAndCount( - req.filterableFields, - req.listConfig - ) + ;[priceLists, count] = await priceListService.listAndCount( + req.filterableFields, + req.listConfig + ) + } res.json({ - price_lists, + price_lists: priceLists, count, offset: validated.offset, limit: validated.limit, diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts new file mode 100644 index 0000000000..3f13f6fb47 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts @@ -0,0 +1,29 @@ +import { MedusaContainer } from "@medusajs/types" +import { PriceList } from "../../../../../models" +import { MedusaError } from "medusa-core-utils" +import { listAndCountPriceListPricingModule } from "./list-and-count-price-lists" + +export async function getPriceListPricingModule( + id: string, + { + container, + }: { + container: MedusaContainer + } +): Promise { + const [priceLists, count] = await listAndCountPriceListPricingModule({ + filters: { + id: [id], + }, + container, + }) + + if (count === 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Price list with id: ${id} was not found` + ) + } + + return priceLists[0] +} diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts new file mode 100644 index 0000000000..f97cec663c --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts @@ -0,0 +1,2 @@ +export * from "./get-price-list" +export * from "./list-and-count-price-lists" diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts new file mode 100644 index 0000000000..179d160445 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts @@ -0,0 +1,104 @@ +import { FilterablePriceListProps, MedusaContainer } from "@medusajs/types" +import { FindConfig } from "../../../../../types/common" +import { CustomerGroup, MoneyAmount, PriceList } from "../../../../../models" +import { CustomerGroupService } from "../../../../../services" +import { defaultAdminPriceListRemoteQueryObject } from "../index" + +export async function listAndCountPriceListPricingModule({ + filters, + listConfig = { skip: 0 }, + container, +}: { + container: MedusaContainer + filters?: FilterablePriceListProps + listConfig?: FindConfig +}): Promise<[PriceList[], number]> { + const remoteQuery = container.resolve("remoteQuery") + const customerGroupService: CustomerGroupService = container.resolve( + "customerGroupService" + ) + + const query = { + price_list: { + __args: { filters, ...listConfig }, + ...defaultAdminPriceListRemoteQueryObject, + }, + } + + const { + rows: priceLists, + metadata: { count }, + } = await remoteQuery(query) + + if (!count) { + return [[], 0] + } + + const customerGroupIds: string[] = priceLists + .map((priceList) => + priceList.price_list_rules + .filter((rule) => rule.rule_type.rule_attribute === "customer_group_id") + .map((rule) => + rule.price_list_rule_values.map((rule_value) => rule_value.value) + ) + ) + .flat(2) + + const priceListCustomerGroups = await customerGroupService.list( + { id: customerGroupIds }, + {} + ) + + const customerGroupIdCustomerGroupMap = new Map( + priceListCustomerGroups.map((customerGroup) => [ + customerGroup.id, + customerGroup, + ]) + ) + + for (const priceList of priceLists) { + const priceSetMoneyAmounts = priceList.price_set_money_amounts || [] + const priceListRulesData = priceList.price_list_rules || [] + delete priceList.price_set_money_amounts + delete priceList.price_list_rules + + priceList.prices = priceSetMoneyAmounts.map((priceSetMoneyAmount) => { + const productVariant = priceSetMoneyAmount.price_set.variant_link.variant + + return { + ...(priceSetMoneyAmount.money_amount as MoneyAmount), + price_list_id: priceList.id, + variant_id: productVariant?.id ?? null, + variant: productVariant ?? null, + region_id: null, + } + }) + + priceList.name = priceList.title + delete priceList.title + + const customerGroupPriceListRule = priceListRulesData.find( + (plr) => plr.rule_type.rule_attribute === "customer_group_id" + ) + + if ( + customerGroupPriceListRule && + customerGroupPriceListRule?.price_list_rule_values + ) { + priceList.customer_groups = + customerGroupPriceListRule?.price_list_rule_values + .map((customerGroupRule) => + customerGroupIdCustomerGroupMap.get(customerGroupRule.value) + ) + .filter( + ( + customerGroup: CustomerGroup | undefined + ): customerGroup is CustomerGroup => !!customerGroup + ) + } else { + priceList.customer_groups = [] + } + } + + return [priceLists, count] +} diff --git a/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts index 9ba8c3a561..b00b582659 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts @@ -1,4 +1,5 @@ -import { PriceListStatus, PriceListType } from "@medusajs/utils" +import { MedusaContainer, PricingTypes, WorkflowTypes } from "@medusajs/types" +import { MedusaV2Flag, PriceListStatus, PriceListType } from "@medusajs/utils" import { IsArray, IsBoolean, @@ -8,15 +9,17 @@ import { ValidateNested, } from "class-validator" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." -import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" +import { updatePriceLists } from "@medusajs/workflows" import { Type } from "class-transformer" import { EntityManager } from "typeorm" import { PriceList } from "../../../.." import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing" import PriceListService from "../../../../services/price-list" +import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { validator } from "../../../../utils/validator" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [post] /admin/price-lists/{id} @@ -83,26 +86,59 @@ import { validator } from "../../../../utils/validator" */ export default async (req, res) => { const { id } = req.params + let priceList + const featureFlagRouter = req.scope.resolve("featureFlagRouter") + const manager: EntityManager = req.scope.resolve("manager") + const priceListService: PriceListService = + req.scope.resolve("priceListService") const validated = await validator( AdminPostPriceListsPriceListPriceListReq, req.body ) - const priceListService: PriceListService = - req.scope.resolve("priceListService") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const updateVariantsWorkflow = updatePriceLists(req.scope) + const rules: PricingTypes.CreatePriceListRules = {} + const customerGroups = validated.customer_groups || [] + delete validated.customer_groups - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .update(id, validated) - }) + if (customerGroups.length) { + rules["customer_group_id"] = customerGroups.map((group) => group.id) + } - const priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + const input = { + price_lists: [ + { + id, + ...validated, + rules, + }, + ], + } as WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowInputDTO + + await updateVariantsWorkflow.run({ + input, + context: { + manager, + }, + }) + + priceList = await getPriceListPricingModule(id, { + container: req.scope as MedusaContainer, + }) + } else { + await manager.transaction(async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .update(id, validated) + }) + + priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/products/create-variant.ts b/packages/medusa/src/api/routes/admin/products/create-variant.ts index ffddb375fb..f394c73db2 100644 --- a/packages/medusa/src/api/routes/admin/products/create-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/create-variant.ts @@ -1,7 +1,6 @@ -import { - CreateProductVariantInput, - ProductVariantPricesCreateReq, -} from "../../../../types/product-variant" +import { IInventoryService, WorkflowTypes } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { CreateProductVariants } from "@medusajs/workflows" import { IsArray, IsBoolean, @@ -11,19 +10,22 @@ import { IsString, ValidateNested, } from "class-validator" +import { defaultAdminProductFields, defaultAdminProductRelations } from "." import { PricingService, ProductService, ProductVariantInventoryService, ProductVariantService, } from "../../../../services" -import { defaultAdminProductFields, defaultAdminProductRelations } from "." - -import { EntityManager } from "typeorm" -import { IInventoryService } from "@medusajs/types" +import { + CreateProductVariantInput, + ProductVariantPricesCreateReq, +} from "../../../../types/product-variant" import { Type } from "class-transformer" -import { createVariantsTransaction } from "./transaction/create-product-variant" +import { EntityManager } from "typeorm" import { validator } from "../../../../utils/validator" +import { getProductWithIsolatedProductModule } from "./get-product" +import { createVariantsTransaction } from "./transaction/create-product-variant" /** * @oas [post] /admin/products/{id}/variants @@ -122,6 +124,8 @@ export default async (req, res) => { req.body ) + const manager: EntityManager = req.scope.resolve("manager") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const inventoryService: IInventoryService | undefined = req.scope.resolve("inventoryService") const productVariantInventoryService: ProductVariantInventoryService = @@ -129,29 +133,56 @@ export default async (req, res) => { const productVariantService: ProductVariantService = req.scope.resolve( "productVariantService" ) - - const manager: EntityManager = req.scope.resolve("manager") - - await manager.transaction(async (transactionManager) => { - await createVariantsTransaction( - { - manager: transactionManager, - inventoryService, - productVariantInventoryService, - productVariantService, - }, - id, - [validated as CreateProductVariantInput] - ) - }) - - const productService: ProductService = req.scope.resolve("productService") const pricingService: PricingService = req.scope.resolve("pricingService") + let rawProduct - const rawProduct = await productService.retrieve(id, { - select: defaultAdminProductFields, - relations: defaultAdminProductRelations, - }) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const createVariantsWorkflow = CreateProductVariants.createProductVariants( + req.scope + ) + + const input = { + productVariants: [ + { + product_id: id, + ...validated, + }, + ] as WorkflowTypes.ProductWorkflow.CreateProductVariantsInputDTO[], + } + + await createVariantsWorkflow.run({ + input, + context: { + manager, + }, + }) + + rawProduct = await getProductWithIsolatedProductModule( + req, + id, + req.retrieveConfig + ) + } else { + await manager.transaction(async (transactionManager) => { + await createVariantsTransaction( + { + manager: transactionManager, + inventoryService, + productVariantInventoryService, + productVariantService, + }, + id, + [validated as CreateProductVariantInput] + ) + }) + + const productService: ProductService = req.scope.resolve("productService") + + rawProduct = await productService.retrieve(id, { + select: defaultAdminProductFields, + relations: defaultAdminProductRelations, + }) + } const [product] = await pricingService.setAdminProductPricing([rawProduct]) diff --git a/packages/medusa/src/api/routes/admin/products/get-product.ts b/packages/medusa/src/api/routes/admin/products/get-product.ts index 9028a0ab9b..b1a238a79e 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.ts +++ b/packages/medusa/src/api/routes/admin/products/get-product.ts @@ -118,7 +118,11 @@ export default async (req, res) => { res.json({ product }) } -async function getProductWithIsolatedProductModule(req, id, retrieveConfig) { +export async function getProductWithIsolatedProductModule( + req, + id, + retrieveConfig +) { // TODO: Add support for fields/expands const remoteQuery = req.scope.resolve("remoteQuery") diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index d63809b423..549b260398 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -7,7 +7,11 @@ import { SalesChannelService, } from "../../../../services" -import { IInventoryService } from "@medusajs/types" +import { + IInventoryService, + IPricingModuleService, + IProductModuleService, +} from "@medusajs/types" import { MedusaV2Flag, promiseAll } from "@medusajs/utils" import { Type } from "class-transformer" import { Product } from "../../../../models" @@ -301,7 +305,53 @@ export default async (req, res) => { }) } -async function listAndCountProductWithIsolatedProductModule( +async function getVariantsFromPriceList(req, priceListId) { + const remoteQuery = req.scope.resolve("remoteQuery") + const pricingModuleService: IPricingModuleService = req.scope.resolve( + "pricingModuleService" + ) + const productModuleService: IProductModuleService = req.scope.resolve( + "productModuleService" + ) + + const [priceList] = await pricingModuleService.listPriceLists( + { id: [priceListId] }, + { + relations: [ + "price_set_money_amounts", + "price_set_money_amounts.price_set", + ], + select: ["price_set_money_amounts.price_set.id"], + } + ) + + const priceSetIds = priceList.price_set_money_amounts?.map( + (psma) => psma.price_set?.id + ) + + const query = { + product_variant_price_set: { + __args: { + price_set_id: priceSetIds, + }, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + const variantIds = variantPriceSets.map((vps) => vps.variant_id) + + return await productModuleService.listVariants( + { + id: variantIds, + }, + { + select: ["product_id"], + } + ) +} + +export async function listAndCountProductWithIsolatedProductModule( req, filterableFields, listConfig @@ -309,6 +359,7 @@ async function listAndCountProductWithIsolatedProductModule( // TODO: Add support for fields/expands const remoteQuery = req.scope.resolve("remoteQuery") + const featureFlagRouter = req.scope.resolve("featureFlagRouter") const productIdsFilter: Set = new Set() const variantIdsFilter: Set = new Set() @@ -352,22 +403,28 @@ async function listAndCountProductWithIsolatedProductModule( delete filterableFields.price_list_id if (priceListId) { - // TODO: it is working but validate the behaviour. - // e.g pricing context properly set. - // At the moment filtering by price list but not having any customer id or - // include discount forces the query to filter with price list id is null - const priceListService = req.scope.resolve( - "priceListService" - ) as PriceListService - promises.push( - priceListService - .listPriceListsVariantIdsMap(priceListId) - .then((priceListVariantIdsMap) => { - priceListVariantIdsMap[priceListId].map((variantId) => - variantIdsFilter.add(variantId) - ) - }) - ) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const variants = await getVariantsFromPriceList(req, priceListId) + + variants.forEach((pv) => variantIdsFilter.add(pv.id)) + } else { + // TODO: it is working but validate the behaviour. + // e.g pricing context properly set. + // At the moment filtering by price list but not having any customer id or + // include discount forces the query to filter with price list id is null + const priceListService = req.scope.resolve( + "priceListService" + ) as PriceListService + promises.push( + priceListService + .listPriceListsVariantIdsMap(priceListId) + .then((priceListVariantIdsMap) => { + priceListVariantIdsMap[priceListId].map((variantId) => + variantIdsFilter.add(variantId) + ) + }) + ) + } } const discountConditionId = filterableFields.discount_condition_id diff --git a/packages/medusa/src/commands/migrate.js b/packages/medusa/src/commands/migrate.js index 981808b5a4..f335104a19 100644 --- a/packages/medusa/src/commands/migrate.js +++ b/packages/medusa/src/commands/migrate.js @@ -5,7 +5,7 @@ import getMigrations, { runIsolatedModulesMigration, } from "./utils/get-migrations" -import { MedusaV2Flag, createMedusaContainer } from "@medusajs/utils" +import { createMedusaContainer } from "@medusajs/utils" import configModuleLoader from "../loaders/config" import databaseLoader from "../loaders/database" import featureFlagLoader from "../loaders/feature-flags" @@ -38,6 +38,11 @@ const getDataSource = async (directory) => { const runLinkMigrations = async (directory) => { const configModule = configModuleLoader(directory) const container = createMedusaContainer() + const featureFlagRouter = featureFlagLoader(configModule) + + container.register({ + featureFlagRouter: asValue(featureFlagRouter), + }) await pgConnectionLoader({ configModule, container }) @@ -63,16 +68,13 @@ const main = async function ({ directory }) { const configModule = configModuleLoader(directory) const dataSource = await getDataSource(directory) - const featureFlagRouter = featureFlagLoader(configModule) if (args[0] === "run") { await dataSource.runMigrations() await dataSource.destroy() await runIsolatedModulesMigration(configModule) - if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - await runLinkMigrations(directory) - } + await runLinkMigrations(directory) process.exit() Logger.info("Migrations completed.") diff --git a/packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts b/packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts new file mode 100644 index 0000000000..be2a73a515 --- /dev/null +++ b/packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts @@ -0,0 +1,20 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { MigrationInterface, QueryRunner } from "typeorm" + +export const featureFlag = MedusaV2Flag.key + +export class DropNonNullConstraintPriceList1699371074198 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE IF EXISTS price_list ALTER COLUMN name DROP NOT NULL; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE IF EXISTS price_list ALTER COLUMN name SET NOT NULL; + `) + } +} diff --git a/packages/medusa/src/models/money-amount.ts b/packages/medusa/src/models/money-amount.ts index db172c17d9..0ecece7eb8 100644 --- a/packages/medusa/src/models/money-amount.ts +++ b/packages/medusa/src/models/money-amount.ts @@ -69,7 +69,7 @@ export class MoneyAmount extends SoftDeletableEntity { @Index("idx_money_amount_region_id") @Column({ nullable: true }) - region_id: string + region_id: string | null @ManyToOne(() => Region) @JoinColumn({ name: "region_id" }) diff --git a/packages/medusa/src/scripts/create-default-rule-types.ts b/packages/medusa/src/scripts/create-default-rule-types.ts index 66e26996b6..e75b96b572 100644 --- a/packages/medusa/src/scripts/create-default-rule-types.ts +++ b/packages/medusa/src/scripts/create-default-rule-types.ts @@ -11,17 +11,28 @@ export const createDefaultRuleTypes = async (container: AwilixContainer) => { "pricingModuleService" ) const existing = await pricingModuleService.listRuleTypes( - { rule_attribute: ["region_id"] }, - { take: 1 } + { rule_attribute: ["region_id", "customer_group_id"] }, + { take: 2 } ) - if (existing.length) { + if (existing.length === 2) { return } - await pricingModuleService.createRuleTypes([ - { name: "region_id", rule_attribute: "region_id" }, - ]) + if (existing.length === 0) { + await pricingModuleService.createRuleTypes([ + { name: "region_id", rule_attribute: "region_id" }, + { name: "customer_group_id", rule_attribute: "customer_group_id" }, + ]) + } else if (existing[0].rule_attribute === "region_id") { + await pricingModuleService.createRuleTypes([ + { name: "customer_group_id", rule_attribute: "customer_group_id" }, + ]) + } else { + await pricingModuleService.createRuleTypes([ + { name: "region_id", rule_attribute: "region_id" }, + ]) + } } const migrate = async function ({ directory }) { @@ -38,7 +49,9 @@ const migrate = async function ({ directory }) { migrate({ directory: process.cwd() }) .then(() => { console.log("Created default rule types") + process.exit() }) .catch(() => { console.log("Failed to create rule types") + process.exit(1) }) diff --git a/packages/medusa/src/scripts/migrate-price-lists.ts b/packages/medusa/src/scripts/migrate-price-lists.ts new file mode 100644 index 0000000000..aaa2cc6834 --- /dev/null +++ b/packages/medusa/src/scripts/migrate-price-lists.ts @@ -0,0 +1,146 @@ +import { IPricingModuleService, PricingTypes } from "@medusajs/types" +import { AwilixContainer } from "awilix" +import dotenv from "dotenv" +import express from "express" +import loaders from "../loaders" +import Logger from "../loaders/logger" +import { PriceListService } from "../services" + +dotenv.config() + +const BATCH_SIZE = 1000 + +export const migratePriceLists = async (container: AwilixContainer) => { + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + + const priceListCoreService: PriceListService = + container.resolve("priceListService") + + const remoteQuery = container.resolve("remoteQuery") + + const existingRuleTypes = await pricingModuleService.listRuleTypes( + { rule_attribute: ["customer_group_id"] }, + { take: 2 } + ) + + if (existingRuleTypes.length === 0) { + Logger.info( + `Run default rules migration before running this migration - node node_modules/@medusajs/medusa/dist/scripts/create-default-rule-types.js` + ) + + return + } + + let offset = 0 + let arePriceListsAvailable = true + + while (arePriceListsAvailable) { + const priceLists = await priceListCoreService.list( + {}, + { + take: BATCH_SIZE, + skip: offset, + relations: ["customer_groups"], + } + ) + + if (priceLists.length === 0) { + break + } + + offset += BATCH_SIZE + + await pricingModuleService.update( + priceLists.map((priceList) => { + const updateData: PricingTypes.UpdatePriceListDTO = { + id: priceList.id, + title: priceList.name, + } + + if (priceList?.customer_groups?.length) { + updateData.rules = { + customer_group_id: priceList.customer_groups.map((cg) => cg.id), + } + } + + return updateData + }) + ) + + for (const priceList of priceLists) { + let productsOffset = 0 + let areVariantsAvailable = true + + while (areVariantsAvailable) { + const [priceListVariants, variantsCount] = + await priceListCoreService.listVariants( + priceList.id, + {}, + { + skip: productsOffset, + take: BATCH_SIZE, + } + ) + + if (variantsCount === 0) { + break + } + + productsOffset += BATCH_SIZE + + const query = { + product_variant_price_set: { + __args: { + variant_id: priceListVariants.map((plv) => plv.id), + }, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + const variantPriceSetMap = new Map( + variantPriceSets.map((mps) => [mps.variant_id, mps.price_set_id]) + ) + + const variantPrices = priceListVariants + .map((plv) => plv.prices || []) + .flat() + + await pricingModuleService.addPriceListPrices([ + { + priceListId: priceList.id, + prices: variantPrices.map((vp) => { + return { + id: vp.id, + price_set_id: variantPriceSetMap.get(vp.variant_id)!, + currency_code: vp.currency_code, + amount: vp.amount, + } + }), + }, + ]) + } + } + } +} + +const migrate = async function ({ directory }) { + const app = express() + const { container } = await loaders({ + directory, + expressApp: app, + isTest: false, + }) + + return await migratePriceLists(container) +} + +migrate({ directory: process.cwd() }) + .then(() => { + console.log("Migrated price lists") + }) + .catch(() => { + console.log("Failed to migrate price lists") + }) diff --git a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts b/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts index 3ff98ac98b..48f888ba41 100644 --- a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts +++ b/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts @@ -45,7 +45,7 @@ const migrateProductVariant = async ( rules: [{ rule_attribute: "region_id" }], prices: variant.prices.map((price) => ({ rules: { - region_id: price.region_id, + ...(price.region_id ? { region_id: price.region_id } : {}), }, currency_code: price.currency_code, min_quantity: price.min_quantity, diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index de1265db5f..79c0752fc7 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -4,15 +4,18 @@ import { PriceSetMoneyAmountDTO, RemoteQueryFunction, } from "@medusajs/types" - +import { + CustomerService, + ProductVariantService, + RegionService, + TaxProviderService, +} from "." import { FlagRouter, MedusaV2Flag, promiseAll, removeNullish, } from "@medusajs/utils" -import { ProductVariantService, RegionService, TaxProviderService } from "." - import { IPriceSelectionStrategy, PriceSelectionContext, @@ -33,11 +36,11 @@ import { TaxedPricing, } from "../types/pricing" -import { MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" -import { TransactionBaseService } from "../interfaces" +import { MedusaError } from "medusa-core-utils" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { TaxServiceRate } from "../types/tax-service" +import { TransactionBaseService } from "../interfaces" import { calculatePriceTaxAmount } from "../utils" type InjectedDependencies = { @@ -45,6 +48,7 @@ type InjectedDependencies = { productVariantService: ProductVariantService taxProviderService: TaxProviderService regionService: RegionService + customerService: CustomerService priceSelectionStrategy: IPriceSelectionStrategy featureFlagRouter: FlagRouter remoteQuery: RemoteQueryFunction @@ -57,6 +61,7 @@ type InjectedDependencies = { class PricingService extends TransactionBaseService { protected readonly regionService: RegionService protected readonly taxProviderService: TaxProviderService + protected readonly customerService_: CustomerService protected readonly priceSelectionStrategy: IPriceSelectionStrategy protected readonly productVariantService: ProductVariantService protected readonly featureFlagRouter: FlagRouter @@ -64,6 +69,7 @@ class PricingService extends TransactionBaseService { protected get pricingModuleService(): IPricingModuleService { return this.__container__.pricingModuleService } + protected get remoteQuery(): RemoteQueryFunction { return this.__container__.remoteQuery } @@ -74,6 +80,7 @@ class PricingService extends TransactionBaseService { regionService, priceSelectionStrategy, featureFlagRouter, + customerService, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -82,6 +89,7 @@ class PricingService extends TransactionBaseService { this.taxProviderService = taxProviderService this.priceSelectionStrategy = priceSelectionStrategy this.productVariantService = productVariantService + this.customerService_ = customerService this.featureFlagRouter = featureFlagRouter } @@ -220,9 +228,20 @@ class PricingService extends TransactionBaseService { (variantPriceSet) => variantPriceSet.price_set_id ) - const queryContext: PriceSelectionContext = removeNullish( - context.price_selection - ) + const queryContext: PriceSelectionContext & { + customer_group_id?: string[] + } = removeNullish(context.price_selection) + + if (queryContext.customer_id) { + const { groups } = await this.customerService_.retrieve( + queryContext.customer_id, + { relations: ["groups"] } + ) + + if (groups?.length) { + queryContext.customer_group_id = groups.map((group) => group.id) + } + } let calculatedPrices: CalculatedPriceSet[] = [] @@ -259,25 +278,37 @@ class PricingService extends TransactionBaseService { } if (priceSetId) { - const calculatedPrice: CalculatedPriceSet | undefined = + const calculatedPrices: CalculatedPriceSet | undefined = calculatedPriceMap.get(priceSetId) - if (calculatedPrice) { - pricingResult.prices = [ - { - id: calculatedPrice.calculated_price?.money_amount_id, - currency_code: calculatedPrice.currency_code, - amount: calculatedPrice.calculated_amount, - min_quantity: calculatedPrice.calculated_price?.min_quantity, - max_quantity: calculatedPrice.calculated_price?.max_quantity, - price_list_id: calculatedPrice.calculated_price?.price_list_id, - }, - ] as MoneyAmount[] + if (calculatedPrices) { + pricingResult.prices.push({ + id: calculatedPrices?.original_price?.money_amount_id, + currency_code: calculatedPrices.currency_code, + amount: calculatedPrices.original_amount, + min_quantity: calculatedPrices.original_price?.min_quantity, + max_quantity: calculatedPrices.original_price?.max_quantity, + price_list_id: calculatedPrices.original_price?.price_list_id, + } as MoneyAmount) - pricingResult.original_price = calculatedPrice?.original_amount - pricingResult.calculated_price = calculatedPrice?.calculated_amount + if ( + calculatedPrices.calculated_price?.money_amount_id !== + calculatedPrices.original_price?.money_amount_id + ) { + pricingResult.prices.push({ + id: calculatedPrices.calculated_price?.money_amount_id, + currency_code: calculatedPrices.currency_code, + amount: calculatedPrices.calculated_amount, + min_quantity: calculatedPrices.calculated_price?.min_quantity, + max_quantity: calculatedPrices.calculated_price?.max_quantity, + price_list_id: calculatedPrices.calculated_price?.price_list_id, + } as MoneyAmount) + } + + pricingResult.original_price = calculatedPrices?.original_amount + pricingResult.calculated_price = calculatedPrices?.calculated_amount pricingResult.calculated_price_type = - calculatedPrice?.calculated_price?.price_list_type + calculatedPrices?.calculated_price?.price_list_type } } diff --git a/packages/medusa/src/types/price-list.ts b/packages/medusa/src/types/price-list.ts index 24ea5e1225..208001af42 100644 --- a/packages/medusa/src/types/price-list.ts +++ b/packages/medusa/src/types/price-list.ts @@ -9,7 +9,7 @@ import { ValidateNested, } from "class-validator" import { PriceList } from "../models/price-list" -import { DateComparisonOperator, FindConfig } from "./common" +import { DateComparisonOperator } from "./common" import { XorConstraint } from "./validators/xor" /** diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts index 659ac3e350..734c96da15 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -1185,6 +1185,77 @@ describe("PricingModule Service - Calculate Price", () => { ]) }) + it("should return best price list price first when price list conditions match", async () => { + await createPriceLists(service) + await createPriceLists( + service, + {}, + {}, + defaultPriceListPrices.map((price) => { + return { ...price, amount: price.amount / 2 } + }) + ) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + region_id: "DE", + customer_group_id: "vip-customer-group-id", + company_id: "medusa-company-id", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + is_calculated_price_price_list: false, + calculated_amount: null, + is_original_price_price_list: false, + original_amount: null, + currency_code: null, + calculated_price: { + money_amount_id: null, + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + original_price: { + money_amount_id: null, + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + }, + { + id: "price-set-PLN", + is_calculated_price_price_list: true, + calculated_amount: 116, + is_original_price_price_list: false, + original_amount: 400, + currency_code: "PLN", + calculated_price: { + money_amount_id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "sale", + min_quantity: null, + max_quantity: null, + }, + original_price: { + money_amount_id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 5, + }, + }, + ]) + }) + it("should return price list prices when price list dont have rules, but context is loaded", async () => { await createPriceLists(service, {}, {}) diff --git a/packages/pricing/src/joiner-config.ts b/packages/pricing/src/joiner-config.ts index ee9c415f72..7be13f7c8b 100644 --- a/packages/pricing/src/joiner-config.ts +++ b/packages/pricing/src/joiner-config.ts @@ -1,12 +1,20 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" -import { Currency, MoneyAmount, PriceSet } from "@models" +import { + Currency, + MoneyAmount, + PriceList, + PriceSet, + PriceSetMoneyAmount, +} from "@models" export const LinkableKeys = { money_amount_id: MoneyAmount.name, currency_code: Currency.name, price_set_id: PriceSet.name, + price_list_id: PriceList.name, + price_set_money_amount_id: PriceSetMoneyAmount.name, } const entityLinkableKeysMap: MapToConfig = {} Object.entries(LinkableKeys).forEach(([key, value]) => { @@ -16,11 +24,12 @@ Object.entries(LinkableKeys).forEach(([key, value]) => { valueFrom: key.split("_").pop()!, }) }) + export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap export const joinerConfig: ModuleJoinerConfig = { serviceName: Modules.PRICING, - primaryKeys: ["id", "currency_code"], + primaryKeys: ["id"], linkableKeys: LinkableKeys, alias: [ { @@ -53,5 +62,17 @@ export const joinerConfig: ModuleJoinerConfig = { methodSuffix: "Currencies", }, }, + { + name: "price_list", + args: { + methodSuffix: "PriceLists", + }, + }, + { + name: "price_lists", + args: { + methodSuffix: "PriceLists", + }, + }, ], } diff --git a/packages/pricing/src/repositories/pricing.ts b/packages/pricing/src/repositories/pricing.ts index 6aadce3886..9e12f4a491 100644 --- a/packages/pricing/src/repositories/pricing.ts +++ b/packages/pricing/src/repositories/pricing.ts @@ -71,6 +71,9 @@ export class PricingRepository price_list_id: "psma1.price_list_id", pl_number_rules: "pl.number_rules", pl_type: "pl.type", + has_price_list: knex.raw( + "case when psma1.price_list_id IS NULL then False else True end" + ), }) .leftJoin("price_set_money_amount as psma1", "psma1.id", "psma1.id") .leftJoin("price_rule as pr", "pr.price_set_money_amount_id", "psma1.id") @@ -154,15 +157,23 @@ export class PricingRepository .leftJoin("rule_type as rt", "rt.id", "pr.rule_type_id") .whereIn("ps.id", pricingFilters.id) .andWhere("ma.currency_code", "=", currencyCode) + .orderBy([ - { column: "price_list_id", order: "asc" }, + { column: "psma.has_price_list", order: "asc" }, { column: "number_rules", order: "desc" }, { column: "default_priority", order: "desc" }, + { column: "amount", order: "asc" }, ]) if (quantity) { priceSetQueryKnex.where("ma.min_quantity", "<=", quantity) priceSetQueryKnex.andWhere("ma.max_quantity", ">=", quantity) + } else { + priceSetQueryKnex.andWhere(function () { + this.andWhere("ma.min_quantity", "<=", "1").orWhereNull( + "ma.min_quantity" + ) + }) } return await priceSetQueryKnex diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 101ad70055..ce623d79ac 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -13,7 +13,15 @@ import { PricingTypes, RuleTypeDTO, } from "@medusajs/types" -import { PriceListType } from "@medusajs/utils" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + MedusaError, + PriceListType, + groupBy, + removeNullish, +} from "@medusajs/utils" import { Currency, @@ -42,15 +50,6 @@ import { PriceSetService, RuleTypeService, } from "@services" - -import { - groupBy, - InjectManager, - InjectTransactionManager, - MedusaContext, - MedusaError, - removeNullish, -} from "@medusajs/utils" import { joinerConfig } from "../joiner-config" import { CreatePriceListRuleValueDTO, PricingRepositoryService } from "../types" @@ -147,6 +146,7 @@ export default class PricingModuleService< pricingContext, sharedContext ) + const pricesSetPricesMap = groupBy(results, "price_set_id") const calculatedPrices = pricingFilters.id.map( @@ -155,8 +155,9 @@ export default class PricingModuleService< // which is prioritized by number_rules first for exact match and then deafult_priority of the rule_type // inject custom price selection here const prices = pricesSetPricesMap.get(priceSetId) || [] - const priceListPrice = prices?.find((p) => !!p.price_list_id) - const defaultPrice = prices?.find((p) => !!!p.price_list_id) + const priceListPrice = prices.find((p) => p.price_list_id) + + const defaultPrice = prices?.find((p) => !p.price_list_id) let calculatedPrice: PricingTypes.CalculatedPriceSetDTO = defaultPrice let originalPrice: PricingTypes.CalculatedPriceSetDTO = defaultPrice 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 abfbb7c579..f2e4f8754b 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 @@ -201,6 +201,7 @@ describe("ProductModuleService products", function () { expect(createdVariant?.options).toHaveLength(1) expect(product.tags).toHaveLength(1) expect(product.variants).toHaveLength(2) + expect(product).toEqual( expect.objectContaining({ id: expect.any(String), diff --git a/packages/product/src/migrations/Migration20230719100648.ts b/packages/product/src/migrations/Migration20230719100648.ts index a99311bc5b..aa66d30890 100644 --- a/packages/product/src/migrations/Migration20230719100648.ts +++ b/packages/product/src/migrations/Migration20230719100648.ts @@ -8,8 +8,11 @@ export class Migration20230719100648 extends Migration { this.addSql( 'create index IF NOT EXISTS "IDX_product_category_path" on "product_category" ("mpath");' ) + + this.addSql('DROP INDEX IF EXISTS "IDX_product_category_handle";') + this.addSql( - 'alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");' + 'alter table "product_category" ADD CONSTRAINT "IDX_product_category_handle" unique ("handle");' ) this.addSql( diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 547052b87a..dbf5f0da9d 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -231,6 +231,82 @@ export default class ProductModuleService< return [JSON.parse(JSON.stringify(variants)), count] } + async createVariants( + data: ProductTypes.CreateProductVariantDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productOptionIds = data + .map((pv) => (pv.options || []).map((opt) => opt.option_id!)) + .flat() + + const productOptions = await this.listOptions( + { id: productOptionIds }, + {}, + sharedContext + ) + + const productOptionsMap = new Map( + productOptions.map((po) => [po.id, po]) + ) + + const productVariantsMap = new Map< + string, + ProductTypes.CreateProductVariantDTO[] + >() + + for (const productVariantData of data) { + productVariantData.options = productVariantData.options?.map((option) => { + const productOption = productOptionsMap.get(option.option_id!) + + return { + option: productOption?.id, + value: option.value, + } + }) + + const productVariants = productVariantsMap.get( + productVariantData.product_id! + ) + + if (productVariants) { + productVariants.push(productVariantData) + } else { + productVariantsMap.set(productVariantData.product_id!, [ + productVariantData, + ]) + } + } + + const productVariants = ( + await promiseAll( + [...productVariantsMap].map(async ([productId, variants]) => { + return await this.productVariantService_.create( + productId, + variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[], + sharedContext + ) + }) + ) + ).flat() + + 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 retrieveTag( tagId: string, diff --git a/packages/product/src/services/product-variant.ts b/packages/product/src/services/product-variant.ts index de7a776f67..06d30a8f02 100644 --- a/packages/product/src/services/product-variant.ts +++ b/packages/product/src/services/product-variant.ts @@ -1,14 +1,14 @@ -import { Product, ProductVariant } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" -import { ProductVariantRepository } from "@repositories" import { InjectManager, InjectTransactionManager, - isString, MedusaContext, ModulesSdkUtils, + isString, retrieveEntity, } from "@medusajs/utils" +import { Product, ProductVariant } from "@models" +import { ProductVariantRepository } from "@repositories" import { ProductVariantServiceTypes } from "../types/services" import ProductService from "./product" @@ -96,7 +96,7 @@ export default class ProductVariantService< if (isString(productOrId)) { product = await this.productService_.retrieve( productOrId, - {}, + { relations: ["variants"] }, sharedContext ) } @@ -105,6 +105,8 @@ export default class ProductVariantService< const data_ = [...data] data_.forEach((variant) => { + delete variant?.product_id + Object.assign(variant, { variant_rank: computedRank++, product, diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 27184f1d79..85d100ff9c 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -13,7 +13,7 @@ export enum ProductStatus { /** * @interface - * + * * A product's data. */ export interface ProductDTO { @@ -152,7 +152,7 @@ export interface ProductDTO { /** * @interface - * + * * A product variant's data. */ export interface ProductVariantDTO { @@ -264,7 +264,7 @@ export interface ProductVariantDTO { /** * @interface - * + * * A product category's data. */ export interface ProductCategoryDTO { @@ -320,7 +320,7 @@ export interface ProductCategoryDTO { /** * @interface - * + * * A product category to create. */ export interface CreateProductCategoryDTO { @@ -356,7 +356,7 @@ export interface CreateProductCategoryDTO { /** * @interface - * + * * The data to update in a product category. */ export interface UpdateProductCategoryDTO { @@ -392,7 +392,7 @@ export interface UpdateProductCategoryDTO { /** * @interface - * + * * A product tag's data. */ export interface ProductTagDTO { @@ -418,7 +418,7 @@ export interface ProductTagDTO { /** * @interface - * + * * A product collection's data. */ export interface ProductCollectionDTO { @@ -452,7 +452,7 @@ export interface ProductCollectionDTO { /** * @interface - * + * * A product type's data. */ export interface ProductTypeDTO { @@ -476,7 +476,7 @@ export interface ProductTypeDTO { /** * @interface - * + * * A product option's data. * */ @@ -513,8 +513,13 @@ export interface ProductOptionDTO { /** * @interface - * + * * The product image's data. + * + * @prop id - The ID of the product image. + * @prop url - The URL of the product image. + * @prop metadata - Holds custom data in key-value pairs. + * @prop deleted_at - When the product image was deleted. */ export interface ProductImageDTO { /** @@ -537,8 +542,15 @@ export interface ProductImageDTO { /** * @interface - * + * * The product option value's data. + * + * @prop id - The ID of the product option value. + * @prop value - The value of the product option value. + * @prop option - The associated product option. It may only be available if the `option` relation is expanded. + * @prop variant - The associated product variant. It may only be available if the `variant` relation is expanded. + * @prop metadata - Holds custom data in key-value pairs. + * @prop deleted_at - When the product option value was deleted. */ export interface ProductOptionValueDTO { /** @@ -573,8 +585,15 @@ export interface ProductOptionValueDTO { /** * @interface - * + * * The filters to apply on retrieved products. + * + * @prop q - Search through the products' attributes, such as titles and descriptions, using this search term. + * @prop handle - The handles to filter products by. + * @prop id - The IDs to filter products by. + * @prop tags - Filters on a product's tags. + * @prop categories - Filters on a product's categories. + * @prop collection_id - Filters a product by its associated collections. */ export interface FilterableProductProps extends BaseFilterable { @@ -628,8 +647,11 @@ export interface FilterableProductProps /** * @interface - * + * * The filters to apply on retrieved product tags. + * + * @prop id - The IDs to filter product tags by. + * @prop value - The value to filter product tags by. */ export interface FilterableProductTagProps extends BaseFilterable { @@ -645,8 +667,11 @@ export interface FilterableProductTagProps /** * @interface - * + * * The filters to apply on retrieved product types. + * + * @prop id - The IDs to filter product types by. + * @prop value - The value to filter product types by. */ export interface FilterableProductTypeProps extends BaseFilterable { @@ -662,8 +687,12 @@ export interface FilterableProductTypeProps /** * @interface - * + * * The filters to apply on retrieved product options. + * + * @prop id - The IDs to filter product options by. + * @prop title - The titles to filter product options by. + * @prop product_id - Filter the product options by their associated products' IDs. */ export interface FilterableProductOptionProps extends BaseFilterable { @@ -683,8 +712,11 @@ export interface FilterableProductOptionProps /** * @interface - * + * * The filters to apply on retrieved product collections. + * + * @prop id - The IDs to filter product collections by. + * @prop title - The title to filter product collections by. */ export interface FilterableProductCollectionProps extends BaseFilterable { @@ -704,8 +736,13 @@ export interface FilterableProductCollectionProps /** * @interface - * + * * The filters to apply on retrieved product variants. + * + * @prop id - The IDs to filter product variants by. + * @prop sku - The SKUs to filter product variants by. + * @prop product_id - Filter the product variants by their associated products' IDs. + * @prop options - Filter product variants by their associated options. */ export interface FilterableProductVariantProps extends BaseFilterable { @@ -734,8 +771,16 @@ export interface FilterableProductVariantProps /** * @interface - * + * * The filters to apply on retrieved product categories. + * + * @prop id - The IDs to filter product categories by. + * @prop name - The names to filter product categories by. + * @prop parent_category_id - Filter product categories by their parent category's ID. + * @prop handle - The handles to filter product categories by. + * @prop is_active - Filter product categories by whether they're active. + * @prop is_internal - Filter product categories by whether they're internal. + * @prop include_descendants_tree - Whether to include children of retrieved product categories. */ export interface FilterableProductCategoryProps extends BaseFilterable { @@ -771,8 +816,13 @@ export interface FilterableProductCategoryProps /** * @interface - * + * * A product collection to create. + * + * @prop title - The product collection's title. + * @prop handle - The product collection's handle. If not provided, the value of this attribute is set to the slug version of the title. + * @prop products - The products to associate with the collection. + * @prop metadata - Holds custom data in key-value pairs. */ export interface CreateProductCollectionDTO { /** @@ -795,7 +845,7 @@ export interface CreateProductCollectionDTO { /** * @interface - * + * * The data to update in a product collection. The `id` is used to identify which product collection to update. */ export interface UpdateProductCollectionDTO { @@ -827,7 +877,7 @@ export interface UpdateProductCollectionDTO { /** * @interface - * + * * A product type to create. */ export interface CreateProductTypeDTO { @@ -852,7 +902,7 @@ export interface UpsertProductTypeDTO { /** * @interface - * + * * The data to update in a product type. The `id` is used to identify which product type to update. */ export interface UpdateProductTypeDTO { @@ -872,7 +922,7 @@ export interface UpdateProductTypeDTO { /** * @interface - * + * * A product tag to create. */ export interface CreateProductTagDTO { @@ -888,9 +938,9 @@ export interface UpsertProductTagDTO { } /** - * + * * @interface - * + * * The data to update in a product tag. The `id` is used to identify which product tag to update. */ export interface UpdateProductTagDTO { @@ -906,7 +956,7 @@ export interface UpdateProductTagDTO { /** * @interface - * + * * A product option to create. */ export interface CreateProductOptionDTO { @@ -928,7 +978,7 @@ export interface UpdateProductOptionDTO { /** * @interface - * + * * A product variant option to create. */ export interface CreateProductVariantOptionDTO { @@ -936,14 +986,19 @@ export interface CreateProductVariantOptionDTO { * The value of a product variant option. */ value: string + option_id?: string } /** * @interface - * + * * A product variant to create. */ export interface CreateProductVariantDTO { + /** + * The id of the product + */ + product_id?: string /** * The tile of the product variant. */ @@ -1020,7 +1075,7 @@ export interface CreateProductVariantDTO { /** * @interface - * + * * The data to update in a product variant. The `id` is used to identify which product variant to update. */ export interface UpdateProductVariantDTO { @@ -1104,7 +1159,7 @@ export interface UpdateProductVariantDTO { /** * @interface - * + * * A product to create. */ export interface CreateProductDTO { @@ -1214,7 +1269,7 @@ export interface CreateProductDTO { /** * @interface - * + * * The data to update in a product. The `id` is used to identify which product to update. */ export interface UpdateProductDTO { @@ -1352,6 +1407,7 @@ export interface CreateProductOnlyDTO { } export interface CreateProductVariantOnlyDTO { + product_id?: string title: string sku?: string barcode?: string diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index 14a67c4260..138b33e6b0 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -5,6 +5,7 @@ import { CreateProductOptionDTO, CreateProductTagDTO, CreateProductTypeDTO, + CreateProductVariantDTO, FilterableProductCategoryProps, FilterableProductCollectionProps, FilterableProductOptionProps, @@ -40,45 +41,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product by its ID - * - * @param {string} productId - The ID of the product to retrieve. - * @param {FindConfig} config - + * + * @param {string} productId - The ID of the product to retrieve. + * @param {FindConfig} config - * The configurations determining how the product is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product. - * + * * @example * A simple example that retrieves a product by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProduct (id: string) { * const productModule = await initializeProductModule() - * + * * const product = await productModule.retrieve(id) - * + * * // do something with the product or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProduct (id: string) { * const productModule = await initializeProductModule() - * + * * const product = await productModule.retrieve(id, { * relations: ["categories"] * }) - * + * * // do something with the product or return it * } * ``` @@ -91,85 +92,85 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of price sets based on optional filters and configuration. - * + * * @param {FilterableProductProps} filters - The filters to apply on the retrieved products. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the products are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of products. - * + * * @example * To retrieve a list of products using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * id: ids * }) - * + * * // do something with the products or return them * } * ``` - * + * * To specify relations that should be retrieved within the products: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * id: ids * }, { * relations: ["categories"] * }) - * + * * // do something with the products or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * id: ids * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * $and: [ * { @@ -181,10 +182,10 @@ export interface IProductModuleService { * ] * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` @@ -197,85 +198,85 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of products along with the total count of available products satisfying the provided filters. - * + * * @param {FilterableProductProps} filters - The filters to apply on the retrieved products. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the products are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of products along with the total count. - * + * * @example * To retrieve a list of products using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * id: ids * }) - * + * * // do something with the products or return them * } * ``` - * + * * To specify relations that should be retrieved within the products: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * id: ids * }, { * relations: ["categories"] * }) - * + * * // do something with the products or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * id: ids * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * $and: [ * { @@ -287,10 +288,10 @@ export interface IProductModuleService { * ] * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` @@ -303,45 +304,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a tag by its ID. - * - * @param {string} tagId - The ID of the tag to retrieve. - * @param {FindConfig} config - + * + * @param {string} tagId - The ID of the tag to retrieve. + * @param {FindConfig} config - * The configurations determining how the product tag is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product tag. - * + * * @example * A simple example that retrieves a product tag by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagId: string) { * const productModule = await initializeProductModule() - * + * * const productTag = await productModule.retrieveTag(tagId) - * + * * // do something with the product tag or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagId: string) { * const productModule = await initializeProductModule() - * + * * const productTag = await productModule.retrieveTag(tagId, { * relations: ["products"] * }) - * + * * // do something with the product tag or return it * } * ``` @@ -354,63 +355,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of tags based on optional filters and configuration. - * + * * @param {FilterableProductTagProps} filters - The filters applied on the retrieved product tags. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product tags are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product tags. - * + * * @example * To retrieve a list of product tags using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * id: tagIds * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * To specify relations that should be retrieved within the product tags: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * id: tagIds * }, { * relations: ["products"] * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * id: tagIds * }, { @@ -418,21 +419,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * $and: [ * { @@ -447,7 +448,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` @@ -460,63 +461,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product tags along with the total count of available product tags satisfying the provided filters. - * + * * @param {FilterableProductTagProps} filters - The filters applied on the retrieved product tags. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product tags are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductTagDTO[], number]>} The list of product tags along with the total count. - * + * * @example * To retrieve a list of product tags using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * id: tagIds * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * To specify relations that should be retrieved within the product tags: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * id: tagIds * }, { * relations: ["products"] * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * id: tagIds * }, { @@ -524,21 +525,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * $and: [ * { @@ -553,7 +554,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` @@ -566,25 +567,25 @@ export interface IProductModuleService { /** * This method is used to create product tags. - * + * * @param {CreateProductTagDTO[]} data - The product tags to create. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product tags. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductTags (values: string[]) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.createTags( * values.map((value) => ({ * value * })) * ) - * + * * // do something with the product tags or return them * } */ @@ -595,26 +596,26 @@ export interface IProductModuleService { /** * This method is used to update existing product tags. - * + * * @param {UpdateProductTagDTO[]} data - The product tags to be updated, each having the attributes that should be updated in a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated product tags. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProductTag (id: string, value: string) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.updateTags([ * { * id, * value * } * ]) - * + * * // do something with the product tags or return them * } */ @@ -625,66 +626,66 @@ export interface IProductModuleService { /** * This method is used to delete product tags by their ID. - * + * * @param {string[]} productTagIds - The IDs of the product tags to be deleted. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product tags are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProductTags (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteTags(ids) - * + * * } */ deleteTags(productTagIds: string[], sharedContext?: Context): Promise /** * This method is used to retrieve a product type by its ID. - * + * * @param {string} typeId - The ID of the product type to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product type is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product type. - * + * * @example * A simple example that retrieves a product type by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductType (id: string) { * const productModule = await initializeProductModule() - * + * * const productType = await productModule.retrieveType(id) - * + * * // do something with the product type or return it * } * ``` - * + * * To specify attributes that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductType (id: string) { * const productModule = await initializeProductModule() - * + * * const productType = await productModule.retrieveType(id, { * select: ["value"] * }) - * + * * // do something with the product type or return it * } * ``` @@ -697,63 +698,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product types based on optional filters and configuration. - * + * * @param {FilterableProductTypeProps} filters - The filters to apply on the retrieved product types. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product types are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product types. - * + * * @example * To retrieve a list of product types using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * id: ids * }) - * + * * // do something with the product types or return them * } * ``` - * + * * To specify attributes that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * id: ids * }, { * select: ["value"] * }) - * + * * // do something with the product types or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * id: ids * }, { @@ -761,21 +762,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * $and: [ * { @@ -790,7 +791,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` @@ -803,63 +804,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product types along with the total count of available product types satisfying the provided filters. - * + * * @param {FilterableProductTypeProps} filters - The filters to be applied on the retrieved product type. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product types are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductTypeDTO[], number]>} The list of product types along with their total count. - * + * * @example * To retrieve a list of product types using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * id: ids * }) - * + * * // do something with the product types or return them * } * ``` - * + * * To specify attributes that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * id: ids * }, { * select: ["value"] * }) - * + * * // do something with the product types or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * id: ids * }, { @@ -867,21 +868,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * $and: [ * { @@ -896,7 +897,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` @@ -909,25 +910,25 @@ export interface IProductModuleService { /** * This method is used to create a product type. - * + * * @param {CreateProductTypeDTO[]} data - The product types to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise} The list of created product types. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductType (value: string) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.createTypes([ * { * value * } * ]) - * + * * // do something with the product types or return them * } */ @@ -938,26 +939,26 @@ export interface IProductModuleService { /** * This method is used to update a product type - * + * * @param {UpdateProductTypeDTO[]} data - The product types to be updated, each having the attributes that should be updated in the product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated product types. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProductType (id: string, value: string) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.updateTypes([ * { * id, * value * } * ]) - * + * * // do something with the product types or return them * } */ @@ -968,19 +969,19 @@ export interface IProductModuleService { /** * This method is used to delete a product type. - * + * * @param {string[]} productTypeIds - The IDs of the product types to be deleted. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product types are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteTypes(ids) * } */ @@ -988,45 +989,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product option by its ID. - * + * * @param {string} optionId - The ID of the product option to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product option is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product option. - * + * * @example * A simple example that retrieves a product option by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOption (id: string) { * const productModule = await initializeProductModule() - * + * * const productOption = await productModule.retrieveOption(id) - * + * * // do something with the product option or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOption (id: string) { * const productModule = await initializeProductModule() - * + * * const productOption = await productModule.retrieveOption(id, { * relations: ["product"] * }) - * + * * // do something with the product option or return it * } * ``` @@ -1039,63 +1040,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product options based on optional filters and configuration. - * + * * @param {FilterableProductOptionProps} filters - The filters applied on the retrieved product options. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product options are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product options. - * + * * @example * To retrieve a list of product options using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * id: ids * }) - * + * * // do something with the product options or return them * } * ``` - * + * * To specify relations that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * id: ids * }, { * relations: ["product"] * }) - * + * * // do something with the product options or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * id: ids * }, { @@ -1103,21 +1104,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * $and: [ * { @@ -1132,7 +1133,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` @@ -1145,63 +1146,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product options along with the total count of available product options satisfying the provided filters. - * + * * @param {FilterableProductOptionProps} filters - The filters applied on the retrieved product options. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product options are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductOptionDTO[], number]>} The list of product options along with the total count. - * + * * @example * To retrieve a list of product options using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * id: ids * }) - * + * * // do something with the product options or return them * } * ``` - * + * * To specify relations that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * id: ids * }, { * relations: ["product"] * }) - * + * * // do something with the product options or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * id: ids * }, { @@ -1209,21 +1210,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * $and: [ * { @@ -1238,7 +1239,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` @@ -1251,26 +1252,26 @@ export interface IProductModuleService { /** * This method is used to create product options. - * + * * @param {CreateProductOptionDTO[]} data - The product options to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {ProductOptionDTO[]} The list of created product options. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductOption (title: string, productId: string) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.createOptions([ * { * title, * product_id: productId * } * ]) - * + * * // do something with the product options or return them * } */ @@ -1281,26 +1282,26 @@ export interface IProductModuleService { /** * This method is used to update existing product options. - * + * * @param {UpdateProductOptionDTO[]} data - The product options to be updated, each holding the attributes that should be updated in the product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {ProductOptionDTO[]} The list of updated product options. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProductOption (id: string, title: string) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.updateOptions([ * { * id, * title * } * ]) - * + * * // do something with the product options or return them * } */ @@ -1311,19 +1312,19 @@ export interface IProductModuleService { /** * This method is used to delete a product option. - * + * * @param {string[]} productOptionIds - The IDs of the product options to delete. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product options are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteOptions(ids) * } */ @@ -1334,45 +1335,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product variant by its ID. - * + * * @param {string} productVariantId - The ID of the product variant to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product variant is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product variant. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product variant. - * + * * @example * A simple example that retrieves a product variant by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariant (id: string) { * const productModule = await initializeProductModule() - * + * * const variant = await productModule.retrieveVariant(id) - * + * * // do something with the product variant or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariant (id: string) { * const productModule = await initializeProductModule() - * + * * const variant = await productModule.retrieveVariant(id, { * relations: ["options"] * }) - * + * * // do something with the product variant or return it * } * ``` @@ -1385,63 +1386,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product variants based on optional filters and configuration. - * + * * @param {FilterableProductVariantProps} filters - The filters applied on the retrieved product variants. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product variants are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product variant. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product variants. - * + * * @example * To retrieve a list of product variants using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * id: ids * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * To specify relations that should be retrieved within the product variants: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * id: ids * }, { * relations: ["options"] * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * id: ids * }, { @@ -1449,21 +1450,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], sku: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * $and: [ * { @@ -1478,7 +1479,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` @@ -1489,65 +1490,93 @@ export interface IProductModuleService { sharedContext?: Context ): Promise + createVariants( + data: CreateProductVariantDTO[], + sharedContext?: Context + ): Promise + + /** + * This method is used to delete ProductVariant. This method will completely remove the ProductVariant and they can no longer be accessed or retrieved. + * + * @param {string[]} productVariantIds - The IDs of the ProductVariant to be deleted. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the ProductVariant are successfully deleted. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function deleteProducts (ids: string[]) { + * const productModule = await initializeProductModule() + * + * await productModule.deleteVariants(ids) + * } + */ + deleteVariants( + productVariantIds: string[], + sharedContext?: Context + ): Promise + /** * This method is used to retrieve a paginated list of product variants along with the total count of available product variants satisfying the provided filters. - * + * * @param {FilterableProductVariantProps} filters - The filters applied on the retrieved product variants. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product variants are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product variant. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductVariantDTO[], number]>} The list of product variants along with their total count. - * + * * @example * To retrieve a list of product variants using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * id: ids * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * To specify relations that should be retrieved within the product variants: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * id: ids * }, { * relations: ["options"] * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * id: ids * }, { @@ -1555,21 +1584,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], sku: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * $and: [ * { @@ -1584,7 +1613,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` @@ -1597,45 +1626,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product collection by its ID. - * + * * @param {string} productCollectionId - The ID of the product collection to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product collection is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product collection. - * + * * @example * A simple example that retrieves a product collection by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollection (id: string) { * const productModule = await initializeProductModule() - * + * * const collection = await productModule.retrieveCollection(id) - * + * * // do something with the product collection or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollection (id: string) { * const productModule = await initializeProductModule() - * + * * const collection = await productModule.retrieveCollection(id, { * relations: ["products"] * }) - * + * * // do something with the product collection or return it * } * ``` @@ -1648,63 +1677,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product collections based on optional filters and configuration. - * + * * @param {FilterableProductCollectionProps} filters - The filters applied on the retrieved product collections. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product collections are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product collections. - * + * * @example * To retrieve a list of product collections using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * id: ids * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * To specify relations that should be retrieved within the product collections: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * id: ids * }, { * relations: ["products"] * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * id: ids * }, { @@ -1712,21 +1741,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * $and: [ * { @@ -1741,7 +1770,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` @@ -1754,63 +1783,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product collections along with the total count of available product collections satisfying the provided filters. - * + * * @param {FilterableProductCollectionProps} filters - The filters applied on the retrieved product collections. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product collections are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductCollectionDTO[], number]>} The list of product collections along with the total count. - * + * * @example * To retrieve a list of product collections using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * id: ids * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * To specify relations that should be retrieved within the product collections: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * id: ids * }, { * relations: ["products"] * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * id: ids * }, { @@ -1818,21 +1847,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * $and: [ * { @@ -1847,7 +1876,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` @@ -1860,28 +1889,28 @@ export interface IProductModuleService { /** * This method is used to create product collections. - * + * * @param {CreateProductCollectionDTO[]} data - The product collections to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of created product collections. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createCollection (title: string) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.createCollections([ * { * title * } * ]) - * + * * // do something with the product collections or return them * } - * + * */ createCollections( data: CreateProductCollectionDTO[], @@ -1890,29 +1919,29 @@ export interface IProductModuleService { /** * This method is used to update existing product collections. - * + * * @param {UpdateProductCollectionDTO[]} data - The product collections to be updated, each holding the attributes that should be updated in the product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated product collections. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateCollection (id: string, title: string) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.updateCollections([ * { * id, * title * } * ]) - * + * * // do something with the product collections or return them * } - * + * */ updateCollections( data: UpdateProductCollectionDTO[], @@ -1921,22 +1950,22 @@ export interface IProductModuleService { /** * This method is used to delete collections by their ID. - * + * * @param {string[]} productCollectionIds - The IDs of the product collections to be updated. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product options are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteCollection (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteCollections(ids) * } - * + * */ deleteCollections( productCollectionIds: string[], @@ -1945,45 +1974,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product category by its ID. - * + * * @param {string} productCategoryId - The ID of the product category to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product category is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product category. - * + * * @example * A simple example that retrieves a product category by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategory (id: string) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.retrieveCategory(id) - * + * * // do something with the product category or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategory (id: string) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.retrieveCategory(id, { * relations: ["parent_category"] * }) - * + * * // do something with the product category or return it * } * ``` @@ -1996,63 +2025,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product categories based on optional filters and configuration. - * + * * @param {FilterableProductCategoryProps} filters - The filters to be applied on the retrieved product categories. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product categories are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product categories. - * + * * @example * To retrieve a list of product categories using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * id: ids * }) - * + * * // do something with the product category or return it * } * ``` - * + * * To specify relations that should be retrieved within the product categories: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * id: ids * }, { * relations: ["parent_category"] * }) - * + * * // do something with the product category or return it * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * id: ids * }, { @@ -2060,21 +2089,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], name: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * $or: [ * { @@ -2089,7 +2118,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` @@ -2102,63 +2131,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product categories along with the total count of available product categories satisfying the provided filters. - * + * * @param {FilterableProductCategoryProps} filters - The filters to apply on the retrieved product categories. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product categories are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductCategoryDTO[], number]>} The list of product categories along with their total count. - * + * * @example * To retrieve a list of product categories using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * id: ids * }) - * + * * // do something with the product category or return it * } * ``` - * + * * To specify relations that should be retrieved within the product categories: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * id: ids * }, { * relations: ["parent_category"] * }) - * + * * // do something with the product category or return it * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * id: ids * }, { @@ -2166,21 +2195,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], name: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * $or: [ * { @@ -2195,7 +2224,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` @@ -2208,27 +2237,27 @@ export interface IProductModuleService { /** * This method is used to create a product category. - * + * * @param {CreateProductCategoryDTO} data - The product category to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created product category. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createCategory (name: string, parent_category_id: string | null) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.createCategory({ * name, * parent_category_id * }) - * + * * // do something with the product category or return it * } - * + * */ createCategory( data: CreateProductCategoryDTO, @@ -2237,24 +2266,24 @@ export interface IProductModuleService { /** * This method is used to update a product category by its ID. - * + * * @param {string} categoryId - The ID of the product category to update. * @param {UpdateProductCategoryDTO} data - The attributes to update in th product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated product category. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateCategory (id: string, name: string) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.updateCategory(id, { * name, * }) - * + * * // do something with the product category or return it * } */ @@ -2266,19 +2295,19 @@ export interface IProductModuleService { /** * This method is used to delete a product category by its ID. - * + * * @param {string} categoryId - The ID of the product category to delete. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product category is successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteCategory (id: string) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteCategory(id) * } */ @@ -2286,25 +2315,25 @@ export interface IProductModuleService { /** * This method is used to create a product. - * + * * @param {CreateProductDTO[]} data - The products to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of created products. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProduct (title: string) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.create([ * { * title * } * ]) - * + * * // do something with the products or return them * } */ @@ -2315,26 +2344,26 @@ export interface IProductModuleService { /** * This method is used to update a product. - * + * * @param {UpdateProductDTO[]} data - The products to be updated, each holding the attributes that should be updated in the product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated products. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProduct (id: string, title: string) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.update([ * { * id, * title * } * ]) - * + * * // do something with the products or return them * } */ @@ -2345,19 +2374,19 @@ export interface IProductModuleService { /** * This method is used to delete products. Unlike the {@link softDelete} method, this method will completely remove the products and they can no longer be accessed or retrieved. - * + * * @param {string[]} productIds - The IDs of the products to be deleted. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the products are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.delete(ids) * } */ @@ -2365,29 +2394,29 @@ export interface IProductModuleService { /** * This method is used to delete products. Unlike the {@link delete} method, this method won't completely remove the product. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter. - * + * * The soft-deleted products can be restored using the {@link restore} method. - * + * * @param {string[]} productIds - The IDs of the products to soft-delete. * @param {SoftDeleteReturn} config - * Configurations determining which relations to soft delete along with the each of the products. You can pass to its `returnLinkableKeys` * property any of the product's relation attribute names, such as `variant_id`. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} + * @returns {Promise | void>} * An object that includes the IDs of related records that were also soft deleted, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of a record associated with the product through this relation, such as the IDs of associated product variants. - * + * * If there are no related records, the promise resolved to `void`. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const cascadedEntities = await productModule.softDelete(ids) - * + * * // do something with the returned cascaded entity IDs or return them * } */ @@ -2399,29 +2428,29 @@ export interface IProductModuleService { /** * This method is used to restore products which were deleted using the {@link softDelete} method. - * + * * @param {string[]} productIds - The IDs of the products to restore. * @param {RestoreReturn} config - * Configurations determining which relations to restore along with the each of the products. You can pass to its `returnLinkableKeys` * property any of the product's relation attribute names, such as `variant_id`. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} + * @returns {Promise | void>} * An object that includes the IDs of related records that were restored, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of the record associated with the product through this relation, such as the IDs of associated product variants. - * + * * If there are no related records that were restored, the promise resolved to `void`. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function restoreProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const cascadedEntities = await productModule.restore(ids, { * returnLinkableKeys: ["variant_id"] * }) - * + * * // do something with the returned cascaded entity IDs or return them * } */ diff --git a/packages/types/src/workflow/index.ts b/packages/types/src/workflow/index.ts index ecce74d8af..57ba877ed9 100644 --- a/packages/types/src/workflow/index.ts +++ b/packages/types/src/workflow/index.ts @@ -2,3 +2,4 @@ export * as CartWorkflow from "./cart" export * as CommonWorkflow from "./common" export * as ProductWorkflow from "./product" export * as InventoryWorkflow from "./inventory" +export * as PriceListWorkflow from "./price-list" diff --git a/packages/types/src/workflow/price-list/create-price-list.ts b/packages/types/src/workflow/price-list/create-price-list.ts new file mode 100644 index 0000000000..024a58ddb1 --- /dev/null +++ b/packages/types/src/workflow/price-list/create-price-list.ts @@ -0,0 +1,78 @@ +import { + CreatePriceListRules, + PriceListRuleDTO, + PriceListStatus, +} from "../../pricing" + +export interface CreatePriceListDTO { + starts_at?: string + ends_at?: string + status?: PriceListStatus + number_rules?: number + rules?: PriceListRuleDTO[] + prices?: { + amount: number + currency_code: string + region_id?: string + max_quantity?: number + min_quantity?: number + }[] + customer_groups?: { + id: string + }[] +} + +export interface CreatePriceListRuleDTO { + rule_attribute: string + value: string[] +} + +export interface CreatePriceListPriceDTO { + amount: number + currency_code: string + price_set_id: string | null + region_id?: string + max_quantity?: number + min_quantity?: number +} + +export interface CreatePriceListWorkflowInputDTO { + price_lists: CreatePriceListWorkflowDTO[] +} + +export interface RemovePriceListProductsWorkflowInputDTO { + product_ids: string[] + price_list_id: string +} + +export interface RemovePriceListVariantsWorkflowInputDTO { + variant_ids: string[] + price_list_id: string +} + +export interface RemovePriceListPricesWorkflowInputDTO { + money_amount_ids: string[] + price_list_id: string +} + +export interface CreatePriceListWorkflowDTO { + title?: string + name: string + description: string + type?: string + starts_at?: string + ends_at?: string + status?: PriceListStatus + number_rules?: number + prices: InputPrice[] + rules?: CreatePriceListRules +} + +interface InputPrice { + region_id?: string + currency_code: string + amount: number + variant_id: string + min_quantity?: number + max_quantity?: number +} diff --git a/packages/types/src/workflow/price-list/index.ts b/packages/types/src/workflow/price-list/index.ts new file mode 100644 index 0000000000..3649befbea --- /dev/null +++ b/packages/types/src/workflow/price-list/index.ts @@ -0,0 +1,3 @@ +export * from "./create-price-list" +export * from "./update-price-list" +export * from "./remove-price-list" diff --git a/packages/types/src/workflow/price-list/remove-price-list.ts b/packages/types/src/workflow/price-list/remove-price-list.ts new file mode 100644 index 0000000000..d6789e631e --- /dev/null +++ b/packages/types/src/workflow/price-list/remove-price-list.ts @@ -0,0 +1,3 @@ +export interface RemovePriceListWorkflowInputDTO { + price_lists: string[] +} diff --git a/packages/types/src/workflow/price-list/update-price-list.ts b/packages/types/src/workflow/price-list/update-price-list.ts new file mode 100644 index 0000000000..a0f035267f --- /dev/null +++ b/packages/types/src/workflow/price-list/update-price-list.ts @@ -0,0 +1,25 @@ +import { CreatePriceListRules, PriceListStatus } from "../../pricing" + +import { UpdateProductVariantPricesInputDTO } from "../product" + +export type PriceListVariantPriceDTO = UpdateProductVariantPricesInputDTO & { + variant_id?: string + price_set_id?: string +} + +export interface UpdatePriceListWorkflowDTO { + id: string + name?: string + starts_at?: string + ends_at?: string + status?: PriceListStatus + rules?: CreatePriceListRules + prices?: PriceListVariantPriceDTO[] + customer_groups?: { + id: string + }[] +} + +export interface UpdatePriceListWorkflowInputDTO { + price_lists: UpdatePriceListWorkflowDTO[] +} diff --git a/packages/types/src/workflow/product/create-product-variants.ts b/packages/types/src/workflow/product/create-product-variants.ts new file mode 100644 index 0000000000..1d7fb0eeba --- /dev/null +++ b/packages/types/src/workflow/product/create-product-variants.ts @@ -0,0 +1,32 @@ +import { + UpsertProductVariantOptionInputDTO, + UpsertProductVariantPricesInputDTO, +} from "./update-product-variants" + +export interface CreateProductVariantsInputDTO { + product_id?: string + title?: string + sku?: string + ean?: string + upc?: string + barcode?: string + hs_code?: string + inventory_quantity?: number + allow_backorder?: boolean + manage_inventory?: boolean + weight?: number + length?: number + height?: number + width?: number + origin_country?: string + mid_code?: string + material?: string + metadata?: Record + + prices?: UpsertProductVariantPricesInputDTO[] + options?: UpsertProductVariantOptionInputDTO[] +} + +export interface CreateProductVariantsWorkflowInputDTO { + productVariants: CreateProductVariantsInputDTO[] +} diff --git a/packages/types/src/workflow/product/index.ts b/packages/types/src/workflow/product/index.ts index f960c39643..6ff12931de 100644 --- a/packages/types/src/workflow/product/index.ts +++ b/packages/types/src/workflow/product/index.ts @@ -1,3 +1,4 @@ +export * from "./create-product-variants" export * from "./create-products" export * from "./update-product-variants" export * from "./update-products" 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 11596a9825..0bcc495237 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -137,6 +137,8 @@ export abstract class MikroOrmAbstractBaseRepository retrieveConstraintsToApply: (q: string) => any[] ): void { if (!("q" in findOptions.where) || !findOptions.where.q) { + delete findOptions.where.q + return } diff --git a/packages/workflows/src/definition/index.ts b/packages/workflows/src/definition/index.ts index 50b5440ede..805f14718f 100644 --- a/packages/workflows/src/definition/index.ts +++ b/packages/workflows/src/definition/index.ts @@ -1,3 +1,4 @@ export * from "./cart" export * from "./product" export * from "./inventory" +export * from './price-list' \ No newline at end of file diff --git a/packages/workflows/src/definition/price-list/create-price-lists.ts b/packages/workflows/src/definition/price-list/create-price-lists.ts new file mode 100644 index 0000000000..0a54a3dbb5 --- /dev/null +++ b/packages/workflows/src/definition/price-list/create-price-lists.ts @@ -0,0 +1,74 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { PricingTypes, WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum CreatePriceListActions { + prepare = "prepare", + createPriceList = "createPriceList", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: CreatePriceListActions.prepare, + noCompensation: true, + next: { + action: CreatePriceListActions.createPriceList, + }, + }, +} + +const handlers = new Map([ + [ + CreatePriceListActions.prepare, + { + invoke: pipe( + { + inputAlias: CreatePriceListActions.prepare, + merge: true, + invoke: { + from: CreatePriceListActions.prepare, + }, + }, + PriceListHandlers.prepareCreatePriceLists + ), + }, + ], + [ + CreatePriceListActions.createPriceList, + { + invoke: pipe( + { + invoke: { + from: CreatePriceListActions.prepare, + alias: PriceListHandlers.createPriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.createPriceLists + ), + compensate: pipe( + { + invoke: { + from: CreatePriceListActions.createPriceList, + alias: PriceListHandlers.removePriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.removePriceLists + ), + }, + ], +]) + +WorkflowManager.register(Workflows.CreatePriceList, workflowSteps, handlers) + +export const createPriceLists = exportWorkflow< + WorkflowTypes.PriceListWorkflow.CreatePriceListWorkflowInputDTO, + { + priceList: PricingTypes.CreatePriceListDTO + }[] +>(Workflows.CreatePriceList, CreatePriceListActions.createPriceList) diff --git a/packages/workflows/src/definition/price-list/index.ts b/packages/workflows/src/definition/price-list/index.ts new file mode 100644 index 0000000000..28dac74acd --- /dev/null +++ b/packages/workflows/src/definition/price-list/index.ts @@ -0,0 +1,5 @@ +export * from "./create-price-lists" +export * from "./remove-price-lists" +export * from "./remove-product-prices" +export * from "./remove-variant-prices" +export * from "./update-price-lists" diff --git a/packages/workflows/src/definition/price-list/remove-price-list-prices.ts b/packages/workflows/src/definition/price-list/remove-price-list-prices.ts new file mode 100644 index 0000000000..086d79cf8f --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-price-list-prices.ts @@ -0,0 +1,71 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum RemovePriceListPricesActions { + prepare = "prepare", + removePriceListPrices = "removePriceListPrices", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemovePriceListPricesActions.prepare, + noCompensation: true, + next: { + action: RemovePriceListPricesActions.removePriceListPrices, + noCompensation: true, + }, + }, +} + +const handlers = new Map([ + [ + RemovePriceListPricesActions.prepare, + { + invoke: pipe( + { + inputAlias: RemovePriceListPricesActions.prepare, + merge: true, + invoke: { + from: RemovePriceListPricesActions.prepare, + }, + }, + PriceListHandlers.prepareRemovePriceListPrices + ), + }, + ], + [ + RemovePriceListPricesActions.removePriceListPrices, + { + invoke: pipe( + { + merge: true, + invoke: { + from: RemovePriceListPricesActions.prepare, + }, + }, + PriceListHandlers.removePrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.RemovePriceListPrices, + workflowSteps, + handlers +) + +export const removePriceListProductPrices = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListPricesWorkflowInputDTO, + string[] +>( + Workflows.RemovePriceListPrices, + RemovePriceListPricesActions.removePriceListPrices +) diff --git a/packages/workflows/src/definition/price-list/remove-price-lists.ts b/packages/workflows/src/definition/price-list/remove-price-lists.ts new file mode 100644 index 0000000000..6a4b3f39dc --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-price-lists.ts @@ -0,0 +1,57 @@ +import { WorkflowTypes } from "@medusajs/types" +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { exportWorkflow, pipe } from "../../helper" + +import { PriceListHandlers } from "../../handlers" +import { Workflows } from "../../definitions" + +export enum RemovePriceListActions { + removePriceList = "removePriceList", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemovePriceListActions.removePriceList, + noCompensation: true, + }, +} + +const handlers = new Map([ + [ + RemovePriceListActions.removePriceList, + { + invoke: pipe( + { + inputAlias: RemovePriceListActions.removePriceList, + merge: true, + invoke: { + from: RemovePriceListActions.removePriceList, + }, + }, + PriceListHandlers.removePriceLists + ), + }, + ], +]) + +WorkflowManager.register(Workflows.DeletePriceLists, workflowSteps, handlers) + +export const removePriceLists = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListWorkflowInputDTO, + { + price_list_ids: string[] + } +>( + Workflows.DeletePriceLists, + RemovePriceListActions.removePriceList, + async (data) => { + return { + price_lists: data.price_lists.map((priceListId) => ({ + price_list: { id: priceListId }, + })), + } + } +) diff --git a/packages/workflows/src/definition/price-list/remove-product-prices.ts b/packages/workflows/src/definition/price-list/remove-product-prices.ts new file mode 100644 index 0000000000..248129c00d --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-product-prices.ts @@ -0,0 +1,72 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum RemoveProductPricesActions { + prepare = "prepare", + removePriceListPriceSetPrices = "removePriceListPriceSetPrices", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemoveProductPricesActions.prepare, + noCompensation: true, + next: { + action: RemoveProductPricesActions.removePriceListPriceSetPrices, + noCompensation: true, + }, + }, +} + +const handlers = new Map([ + [ + RemoveProductPricesActions.prepare, + { + invoke: pipe( + { + inputAlias: RemoveProductPricesActions.prepare, + merge: true, + invoke: { + from: RemoveProductPricesActions.prepare, + }, + }, + PriceListHandlers.prepareRemoveProductPrices + ), + }, + ], + [ + RemoveProductPricesActions.removePriceListPriceSetPrices, + { + invoke: pipe( + { + merge: true, + invoke: { + from: RemoveProductPricesActions.prepare, + alias: PriceListHandlers.createPriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.removePriceListPriceSetPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.RemovePriceListProductPrices, + workflowSteps, + handlers +) + +export const removePriceListProductPrices = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO, + string[] +>( + Workflows.RemovePriceListProductPrices, + RemoveProductPricesActions.removePriceListPriceSetPrices +) diff --git a/packages/workflows/src/definition/price-list/remove-variant-prices.ts b/packages/workflows/src/definition/price-list/remove-variant-prices.ts new file mode 100644 index 0000000000..b68ef60ed7 --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-variant-prices.ts @@ -0,0 +1,72 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum RemoveVariantPricesActions { + prepare = "prepare", + removePriceListPriceSetPrices = "removePriceListPriceSetPrices", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemoveVariantPricesActions.prepare, + noCompensation: true, + next: { + action: RemoveVariantPricesActions.removePriceListPriceSetPrices, + noCompensation: true, + }, + }, +} + +const handlers = new Map([ + [ + RemoveVariantPricesActions.prepare, + { + invoke: pipe( + { + inputAlias: RemoveVariantPricesActions.prepare, + merge: true, + invoke: { + from: RemoveVariantPricesActions.prepare, + }, + }, + PriceListHandlers.prepareRemoveVariantPrices + ), + }, + ], + [ + RemoveVariantPricesActions.removePriceListPriceSetPrices, + { + invoke: pipe( + { + merge: true, + invoke: { + from: RemoveVariantPricesActions.prepare, + alias: PriceListHandlers.createPriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.removePriceListPriceSetPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.RemovePriceListVariantPrices, + workflowSteps, + handlers +) + +export const removePriceListVariantPrices = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListVariantsWorkflowInputDTO, + string[] +>( + Workflows.RemovePriceListVariantPrices, + RemoveVariantPricesActions.removePriceListPriceSetPrices +) diff --git a/packages/workflows/src/definition/price-list/update-price-lists.ts b/packages/workflows/src/definition/price-list/update-price-lists.ts new file mode 100644 index 0000000000..a445714199 --- /dev/null +++ b/packages/workflows/src/definition/price-list/update-price-lists.ts @@ -0,0 +1,65 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum UpdatePriceListActions { + prepare = "prepare", + updatePriceList = "updatePriceList", +} + +const workflowSteps: TransactionStepsDefinition = { + action: UpdatePriceListActions.prepare, + noCompensation: true, + next: { + next: { + noCompensation: true, + action: UpdatePriceListActions.updatePriceList, + }, + }, +} + +const handlers = new Map([ + [ + UpdatePriceListActions.prepare, + { + invoke: pipe( + { + inputAlias: UpdatePriceListActions.prepare, + merge: true, + invoke: { + from: UpdatePriceListActions.prepare, + }, + }, + PriceListHandlers.prepareUpdatePriceLists + ), + }, + ], + [ + UpdatePriceListActions.updatePriceList, + { + invoke: pipe( + { + inputAlias: UpdatePriceListActions.updatePriceList, + merge: true, + invoke: { + from: UpdatePriceListActions.prepare, + }, + }, + PriceListHandlers.updatePriceLists + ), + }, + ], +]) + +WorkflowManager.register(Workflows.UpdatePriceLists, workflowSteps, handlers) + +export const updatePriceLists = exportWorkflow< + WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowInputDTO, + { priceList: WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowDTO }[] +>(Workflows.UpdatePriceLists, UpdatePriceListActions.updatePriceList) diff --git a/packages/workflows/src/definition/product/create-product-variants.ts b/packages/workflows/src/definition/product/create-product-variants.ts new file mode 100644 index 0000000000..0322af48a2 --- /dev/null +++ b/packages/workflows/src/definition/product/create-product-variants.ts @@ -0,0 +1,121 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { InputAlias, Workflows } from "../../definitions" +import { exportWorkflow, pipe } from "../../helper" + +import { ProductTypes, WorkflowTypes } from "@medusajs/types" +import { ProductHandlers } from "../../handlers" + +export enum CreateProductVariantsActions { + prepare = "prepare", + createProductVariants = "createProductVariants", + revertProductVariantsCreate = "revertProductVariantsCreate", + upsertPrices = "upsertPrices", +} + +export const workflowSteps: TransactionStepsDefinition = { + next: { + action: CreateProductVariantsActions.prepare, + noCompensation: true, + next: { + action: CreateProductVariantsActions.createProductVariants, + next: [ + { + action: CreateProductVariantsActions.upsertPrices, + }, + ], + }, + }, +} + +const handlers = new Map([ + [ + CreateProductVariantsActions.prepare, + { + invoke: pipe( + { + merge: true, + inputAlias: InputAlias.ProductVariantsCreateInputData, + invoke: { + from: InputAlias.ProductVariantsCreateInputData, + }, + }, + ProductHandlers.createProductVariantsPrepareData + ), + }, + ], + [ + CreateProductVariantsActions.createProductVariants, + { + invoke: pipe( + { + merge: true, + invoke: { + from: CreateProductVariantsActions.prepare, + }, + }, + ProductHandlers.createProductVariants + ), + compensate: pipe( + { + merge: true, + invoke: [ + { + from: CreateProductVariantsActions.prepare, + }, + { + from: CreateProductVariantsActions.createProductVariants, + }, + ], + }, + ProductHandlers.removeProductVariants + ), + }, + ], + [ + CreateProductVariantsActions.upsertPrices, + { + invoke: pipe( + { + merge: true, + invoke: [ + { + from: CreateProductVariantsActions.createProductVariants, + }, + ], + }, + ProductHandlers.upsertVariantPrices + ), + compensate: pipe( + { + merge: true, + invoke: [ + { + from: CreateProductVariantsActions.prepare, + }, + { + from: CreateProductVariantsActions.upsertPrices, + }, + ], + }, + ProductHandlers.revertVariantPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.CreateProductVariants, + workflowSteps, + handlers +) + +export const createProductVariants = exportWorkflow< + WorkflowTypes.ProductWorkflow.CreateProductVariantsWorkflowInputDTO, + ProductTypes.ProductVariantDTO[] +>( + Workflows.CreateProductVariants, + CreateProductVariantsActions.createProductVariants +) diff --git a/packages/workflows/src/definition/product/index.ts b/packages/workflows/src/definition/product/index.ts index ab2a40334d..c138e52cdd 100644 --- a/packages/workflows/src/definition/product/index.ts +++ b/packages/workflows/src/definition/product/index.ts @@ -1,3 +1,4 @@ +export * as CreateProductVariants from "./create-product-variants" export * from "./create-products" export * as UpdateProductVariants from "./update-product-variants" export * from "./update-products" diff --git a/packages/workflows/src/definitions.ts b/packages/workflows/src/definitions.ts index 5f4fa84616..645b88ce4e 100644 --- a/packages/workflows/src/definitions.ts +++ b/packages/workflows/src/definitions.ts @@ -4,12 +4,21 @@ export enum Workflows { UpdateProducts = "update-products", // Product Variant workflows + CreateProductVariants = "create-product-variants", UpdateProductVariants = "update-product-variants", // Cart workflows CreateCart = "create-cart", CreateInventoryItems = "create-inventory-items", + + // Price list workflows + CreatePriceList = "create-price-list", + UpdatePriceLists = "update-price-lists", + DeletePriceLists = "delete-price-lists", + RemovePriceListProductPrices = "remove-price-list-products", + RemovePriceListVariantPrices = "remove-price-list-variants", + RemovePriceListPrices = "remove-price-list-prices", } export enum InputAlias { @@ -19,6 +28,7 @@ export enum InputAlias { ProductVariants = "productVariants", ProductVariantsUpdateInputData = "productVariantsUpdateInputData", + ProductVariantsCreateInputData = "productVariantsCreateInputData", InventoryItems = "inventoryItems", RemovedInventoryItems = "removedInventoryItems", diff --git a/packages/workflows/src/handlers/index.ts b/packages/workflows/src/handlers/index.ts index 5c0282342f..2c3607b667 100644 --- a/packages/workflows/src/handlers/index.ts +++ b/packages/workflows/src/handlers/index.ts @@ -4,6 +4,7 @@ export * as CommonHandlers from "./common" export * as CustomerHandlers from "./customer" export * as InventoryHandlers from "./inventory" export * as MiddlewaresHandlers from "./middlewares" +export * as PriceListHandlers from "./price-list" export * as ProductHandlers from "./product" export * as RegionHandlers from "./region" export * as SalesChannelHandlers from "./sales-channel" diff --git a/packages/workflows/src/handlers/price-list/create-price-list.ts b/packages/workflows/src/handlers/price-list/create-price-list.ts new file mode 100644 index 0000000000..1e69066082 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/create-price-list.ts @@ -0,0 +1,42 @@ +import { + CreatePriceListDTO, + IPricingModuleService, + PriceListDTO, +} from "@medusajs/types" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { WorkflowArguments } from "../../helper" + +type Result = { + priceList: PriceListDTO +}[] + +type Input = { + tag?: string + priceList: CreatePriceListDTO +}[] + +export async function createPriceLists({ + container, + data, +}: WorkflowArguments<{ + priceLists: Input +}>): Promise { + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + return await Promise.all( + data.priceLists.map(async (item) => { + const [priceList] = await pricingService!.createPriceLists([ + item.priceList, + ]) + + return { tag: item.tag ?? priceList.id, priceList } + }) + ) +} + +createPriceLists.aliases = { + priceLists: "priceLists", +} diff --git a/packages/workflows/src/handlers/price-list/index.ts b/packages/workflows/src/handlers/price-list/index.ts new file mode 100644 index 0000000000..9f95fd8c6b --- /dev/null +++ b/packages/workflows/src/handlers/price-list/index.ts @@ -0,0 +1,10 @@ +export * from "./create-price-list" +export * from "./prepare-create-price-list" +export * from "./prepare-update-price-lists" +export * from "./remove-price-list" +export * from "./update-price-lists" +export * from "./prepare-remove-product-prices" +export * from "./remove-price-set-price-list-prices" +export * from "./prepare-remove-variant-prices" +export * from "./prepare-remove-price-list-prices" +export * from "./remove-prices" diff --git a/packages/workflows/src/handlers/price-list/prepare-create-price-list.ts b/packages/workflows/src/handlers/price-list/prepare-create-price-list.ts new file mode 100644 index 0000000000..327badb612 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-create-price-list.ts @@ -0,0 +1,87 @@ +import { CreatePriceListDTO, PriceListWorkflow } from "@medusajs/types" +import { MedusaError } from "@medusajs/utils" +import { WorkflowArguments } from "../../helper" + +type Result = { + tag?: string + priceList: CreatePriceListDTO +}[] + +export async function prepareCreatePriceLists({ + container, + data, +}: WorkflowArguments<{ + price_lists: (PriceListWorkflow.CreatePriceListWorkflowDTO & { + _associationTag?: string + })[] +}>): Promise { + const remoteQuery = container.resolve("remoteQuery") + + const { price_lists } = data + + const variantIds = price_lists + .map((priceList) => priceList.prices.map((price) => price.variant_id)) + .flat() + + const variables = { + variant_id: variantIds, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + + const variantIdPriceSetIdMap: Map = new Map( + variantPriceSets.map((variantPriceSet) => [ + variantPriceSet.variant_id, + variantPriceSet.price_set_id, + ]) + ) + + const variantsWithoutPriceSets: string[] = [] + + for (const variantId of variantIds) { + if (!variantIdPriceSetIdMap.has(variantId)) { + variantsWithoutPriceSets.push(variantId) + } + } + + if (variantsWithoutPriceSets.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `No priceSet exist for variants: ${variantsWithoutPriceSets.join(", ")}` + ) + } + + return price_lists.map((priceListDTO) => { + priceListDTO.title ??= priceListDTO.name + const { _associationTag, name, prices, ...rest } = priceListDTO + + const priceList = rest as CreatePriceListDTO + + priceList.rules ??= {} + priceList.prices = + prices?.map((price) => { + const price_set_id = variantIdPriceSetIdMap.get(price.variant_id)! + + return { + currency_code: price.currency_code, + amount: price.amount, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + price_set_id, + } + }) ?? [] + + return { priceList, tag: _associationTag } + }) +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts b/packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts new file mode 100644 index 0000000000..e31a63cb18 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts @@ -0,0 +1,48 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPricingModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type Result = { + moneyAmountIds: string[] + priceListId: string +} + +export async function prepareRemovePriceListPrices({ + container, + data, +}: WorkflowArguments<{ + money_amount_ids: string[] + price_list_id: string +}>): Promise { + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + const { + price_list_id: priceListId, + money_amount_ids: moneyAmountIdsToDelete, + } = data + + const moneyAmounts = await pricingService.listMoneyAmounts( + { id: moneyAmountIdsToDelete }, + { + relations: [ + "price_set_money_amount", + "price_set_money_amount.price_list", + ], + } + ) + + const moneyAmountIds = moneyAmounts + .filter( + (moneyAmount) => + moneyAmount?.price_set_money_amount?.price_list?.id === priceListId + ) + .map((ma) => ma.id) + + return { moneyAmountIds, priceListId } +} + +prepareRemovePriceListPrices.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts b/packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts new file mode 100644 index 0000000000..34b22b9000 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts @@ -0,0 +1,62 @@ +import { WorkflowArguments } from "../../helper" +import { prepareCreatePriceLists } from "./prepare-create-price-list" + +type Result = { + priceSetIds: string[] + priceListId: string +} + +export async function prepareRemoveProductPrices({ + container, + data, +}: WorkflowArguments<{ + product_ids: string[] + price_list_id: string +}>): Promise { + const remoteQuery = container.resolve("remoteQuery") + + const { price_list_id, product_ids } = data + + const variables = { + id: product_ids, + } + + const query = { + product: { + __args: variables, + ...defaultAdminProductRemoteQueryObject, + }, + } + + const productsWithVariantPriceSets: QueryResult[] = await remoteQuery(query) + + const priceSetIds = productsWithVariantPriceSets + .map(({ variants }) => variants.map(({ price }) => price.price_set_id)) + .flat() + + return { priceSetIds, priceListId: price_list_id } +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} + +type QueryResult = { + id: string + variants: { + id: string + price: { + price_set_id: string + variant_id: string + } + }[] +} + +const defaultAdminProductRemoteQueryObject = { + fields: ["id"], + variants: { + price: { + fields: ["variant_id", "price_set_id"], + }, + }, +} diff --git a/packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts b/packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts new file mode 100644 index 0000000000..d903fd459f --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts @@ -0,0 +1,56 @@ +import { WorkflowArguments } from "../../helper" +import { prepareCreatePriceLists } from "./prepare-create-price-list" + +type Result = { + priceSetIds: string[] + priceListId: string +} + +export async function prepareRemoveVariantPrices({ + container, + data, +}: WorkflowArguments<{ + variant_ids: string[] + price_list_id: string +}>): Promise { + const remoteQuery = container.resolve("remoteQuery") + + const { price_list_id, variant_ids } = data + + const variables = { + variant_id: variant_ids, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const productsWithVariantPriceSets: QueryResult[] = await remoteQuery(query) + + const priceSetIds = productsWithVariantPriceSets.map( + (variantPriceSet) => variantPriceSet.price_set_id + ) + + return { priceSetIds, priceListId: price_list_id } +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} + +type QueryResult = { + price_set_id: string + variant_id: string +} + +const defaultAdminProductRemoteQueryObject = { + fields: ["id"], + variants: { + price: { + fields: ["variant_id", "price_set_id"], + }, + }, +} diff --git a/packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts b/packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts new file mode 100644 index 0000000000..490974b395 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts @@ -0,0 +1,87 @@ +import { + PriceListPriceDTO, + UpdatePriceListDTO, + WorkflowTypes, +} from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type Result = { + priceLists: UpdatePriceListDTO[] + priceListPricesMap: Map +} + +export async function prepareUpdatePriceLists({ + container, + data, +}: WorkflowArguments<{ + price_lists: WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowDTO[] +}>): Promise { + const { price_lists: priceListsData } = data + const remoteQuery = container.resolve("remoteQuery") + + const variantPriceSetMap = new Map() + const priceListPricesMap = new Map() + + const variantIds = priceListsData + .map((priceListData) => priceListData.prices?.map((p) => p.variant_id)) + .flat() + + const variables = { + variant_id: variantIds, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + + for (const { variant_id, price_set_id } of variantPriceSets) { + variantPriceSetMap.set(variant_id, price_set_id) + } + + const priceLists = priceListsData.map((priceListData) => { + const priceListPrices: PriceListPriceDTO[] = [] + + priceListData.prices?.forEach((price) => { + const { variant_id, ...priceData } = price + if (!variant_id) { + return + } + + priceListPrices.push({ + id: priceData.id, + price_set_id: variantPriceSetMap.get(variant_id) as string, + currency_code: priceData.currency_code as string, + amount: priceData.amount, + min_quantity: priceData.min_quantity, + max_quantity: priceData.max_quantity, + }) + + return + }) + + priceListPricesMap.set(priceListData.id, priceListPrices) + + delete priceListData?.prices + + const priceListDataClone: UpdatePriceListDTO = { + ...priceListData, + } + + if (priceListData.name) { + priceListDataClone.title = priceListData.name + } + + return priceListDataClone + }) + + return { priceLists, priceListPricesMap } +} + +prepareUpdatePriceLists.aliases = { + payload: "prepare", +} diff --git a/packages/workflows/src/handlers/price-list/remove-price-list.ts b/packages/workflows/src/handlers/price-list/remove-price-list.ts new file mode 100644 index 0000000000..b38bd92cca --- /dev/null +++ b/packages/workflows/src/handlers/price-list/remove-price-list.ts @@ -0,0 +1,31 @@ +import { IPricingModuleService, PriceListDTO } from "@medusajs/types" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { WorkflowArguments } from "../../helper" + +export async function removePriceLists({ + container, + data, +}: WorkflowArguments<{ + price_lists: { + price_list: PriceListDTO + }[] +}>): Promise< + { + price_list: PriceListDTO + }[] +> { + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + await pricingService!.deletePriceLists( + data.price_lists.map(({ price_list }) => price_list.id) + ) + + return data.price_lists +} + +removePriceLists.aliases = { + priceLists: "priceLists", +} diff --git a/packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts b/packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts new file mode 100644 index 0000000000..09ce0a382d --- /dev/null +++ b/packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts @@ -0,0 +1,39 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPricingModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" +import { prepareCreatePriceLists } from "./prepare-create-price-list" + +export async function removePriceListPriceSetPrices({ + container, + data, +}: WorkflowArguments<{ + priceSetIds: string[] + priceListId: string +}>): Promise { + const { priceSetIds, priceListId } = data + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + const priceSetMoneyAmounts = await pricingService.listPriceSetMoneyAmounts( + { + price_set_id: priceSetIds, + price_list_id: [priceListId], + }, + { + relations: ["money_amount"], + } + ) + + const moneyAmountIDs = priceSetMoneyAmounts + .map((priceSetMoneyAmount) => priceSetMoneyAmount.money_amount?.id) + .filter((moneyAmountId): moneyAmountId is string => !!moneyAmountId) + + await pricingService.deleteMoneyAmounts(moneyAmountIDs) + + return moneyAmountIDs +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/remove-prices.ts b/packages/workflows/src/handlers/price-list/remove-prices.ts new file mode 100644 index 0000000000..159f74050e --- /dev/null +++ b/packages/workflows/src/handlers/price-list/remove-prices.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPricingModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type Result = { + deletedPriceIds: string[] +} + +export async function removePrices({ + container, + data, +}: WorkflowArguments<{ + moneyAmountIds: string[] +}>): Promise { + const { moneyAmountIds } = data + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + await pricingService.deleteMoneyAmounts(moneyAmountIds) + + return { + deletedPriceIds: moneyAmountIds, + } +} + +removePrices.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/update-price-lists.ts b/packages/workflows/src/handlers/price-list/update-price-lists.ts new file mode 100644 index 0000000000..4e62603e47 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/update-price-lists.ts @@ -0,0 +1,63 @@ +import { + AddPriceListPricesDTO, + IPricingModuleService, + PriceListDTO, + PriceListPriceDTO, + UpdateMoneyAmountDTO, + UpdatePriceListDTO, +} from "@medusajs/types" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { WorkflowArguments } from "../../helper" + +type Result = { + priceLists: PriceListDTO[] +} + +export async function updatePriceLists({ + container, + data, +}: WorkflowArguments<{ + priceLists: UpdatePriceListDTO[] + priceListPricesMap: Map +}>): Promise { + const { priceLists: priceListsData, priceListPricesMap } = data + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + const priceLists = await pricingService.updatePriceLists(priceListsData) + const addPriceListPricesData: AddPriceListPricesDTO[] = [] + const moneyAmountsToUpdate: UpdateMoneyAmountDTO[] = [] + + for (const [priceListId, prices] of priceListPricesMap.entries()) { + const moneyAmountsToCreate: PriceListPriceDTO[] = [] + + for (const price of prices) { + if (price.id) { + moneyAmountsToUpdate.push(price as UpdateMoneyAmountDTO) + } else { + moneyAmountsToCreate.push(price) + } + } + + addPriceListPricesData.push({ + priceListId, + prices: moneyAmountsToCreate, + }) + } + + if (addPriceListPricesData.length) { + await pricingService.addPriceListPrices(addPriceListPricesData) + } + + if (moneyAmountsToUpdate.length) { + await pricingService.updateMoneyAmounts(moneyAmountsToUpdate) + } + + return { priceLists } +} + +updatePriceLists.aliases = { + payload: "updatePriceLists", +} diff --git a/packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts b/packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts new file mode 100644 index 0000000000..daadc4d827 --- /dev/null +++ b/packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts @@ -0,0 +1,67 @@ +import { ProductWorkflow, WorkflowTypes } from "@medusajs/types" + +import { WorkflowArguments } from "../../helper" + +type VariantPrice = { + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + +export type CreateProductVariantsPreparedData = { + productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] + variantIndexPricesMap: Map + productVariantsMap: Map< + string, + ProductWorkflow.CreateProductVariantsInputDTO[] + > +} + +export async function createProductVariantsPrepareData({ + container, + data, +}: WorkflowArguments): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] = + data.productVariants || [] + + const variantIndexPricesMap = new Map() + const productVariantsMap = new Map< + string, + ProductWorkflow.CreateProductVariantsInputDTO[] + >() + + for (const [index, productVariantData] of productVariants.entries()) { + if (!productVariantData.product_id) { + continue + } + + variantIndexPricesMap.set(index, productVariantData.prices || []) + + delete productVariantData.prices + + const productVariants = productVariantsMap.get( + productVariantData.product_id + ) + + if (productVariants) { + productVariants.push(productVariantData) + } else { + productVariantsMap.set(productVariantData.product_id, [ + productVariantData, + ]) + } + } + + return { + productVariants, + variantIndexPricesMap, + productVariantsMap, + } +} + +createProductVariantsPrepareData.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/product/create-product-variants.ts b/packages/workflows/src/handlers/product/create-product-variants.ts new file mode 100644 index 0000000000..054877d5a5 --- /dev/null +++ b/packages/workflows/src/handlers/product/create-product-variants.ts @@ -0,0 +1,46 @@ +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type VariantPrice = { + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + +type HandlerInput = { + productVariantsMap: Map + variantIndexPricesMap: Map +} + +export async function createProductVariants({ + container, + data, +}: WorkflowArguments): Promise<{ + productVariants: ProductTypes.ProductVariantDTO[] + variantPricesMap: Map +}> { + const { productVariantsMap, variantIndexPricesMap } = data + const variantPricesMap = new Map() + const productModuleService: ProductTypes.IProductModuleService = + container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) + + const productVariants = await productModuleService.createVariants( + [...productVariantsMap.values()].flat() + ) + + productVariants.forEach((variant, index) => { + variantPricesMap.set(variant.id, variantIndexPricesMap.get(index) || []) + }) + + return { + productVariants, + variantPricesMap, + } +} + +createProductVariants.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/product/create-products-prepare-data.ts b/packages/workflows/src/handlers/product/create-products-prepare-data.ts index b6b2f1c0c6..ebce044b59 100644 --- a/packages/workflows/src/handlers/product/create-products-prepare-data.ts +++ b/packages/workflows/src/handlers/product/create-products-prepare-data.ts @@ -1,8 +1,8 @@ import { ProductTypes, SalesChannelTypes, WorkflowTypes } from "@medusajs/types" import { FeatureFlagUtils, - ShippingProfileUtils, kebabCase, + ShippingProfileUtils, } from "@medusajs/utils" import { WorkflowArguments } from "../../helper" @@ -114,23 +114,17 @@ export async function createProductsPrepareData({ } if (product.variants) { - const hasPrices = product.variants.some((variant) => { - return (variant.prices?.length ?? 0) > 0 + const items = + productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] + + product.variants.forEach((variant, index) => { + items.push({ + index, + prices: variant.prices!, + }) }) - if (hasPrices) { - const items = - productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] - - product.variants.forEach((variant, index) => { - items.push({ - index, - prices: variant.prices!, - }) - }) - - productsHandleVariantsIndexPricesMap.set(product.handle!, items) - } + productsHandleVariantsIndexPricesMap.set(product.handle!, items) } } diff --git a/packages/workflows/src/handlers/product/index.ts b/packages/workflows/src/handlers/product/index.ts index 697c0e0b83..bfadce740d 100644 --- a/packages/workflows/src/handlers/product/index.ts +++ b/packages/workflows/src/handlers/product/index.ts @@ -1,10 +1,13 @@ export * from "./attach-sales-channel-to-products" export * from "./attach-shipping-profile-to-products" +export * from "./create-product-variants" +export * from "./create-product-variants-prepare-data" export * from "./create-products" export * from "./create-products-prepare-data" export * from "./detach-sales-channel-from-products" export * from "./detach-shipping-profile-from-products" export * from "./list-products" +export * from "./remove-product-variants" export * from "./remove-products" export * from "./revert-update-products" export * from "./revert-variant-prices" diff --git a/packages/workflows/src/handlers/product/remove-product-variants.ts b/packages/workflows/src/handlers/product/remove-product-variants.ts new file mode 100644 index 0000000000..d40edf7a4b --- /dev/null +++ b/packages/workflows/src/handlers/product/remove-product-variants.ts @@ -0,0 +1,26 @@ +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { IProductModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type HandlerInput = { productVariants: { id: string }[] } + +export async function removeProductVariants({ + container, + data, +}: WorkflowArguments): Promise { + if (!data.productVariants.length) { + return + } + + const productModuleService: IProductModuleService = container.resolve( + ModulesDefinition[Modules.PRODUCT].registrationName + ) + + await productModuleService.deleteVariants( + data.productVariants.map((p) => p.id) + ) +} + +removeProductVariants.aliases = { + productVariants: "productVariants", +} diff --git a/packages/workflows/src/handlers/product/upsert-variant-prices.ts b/packages/workflows/src/handlers/product/upsert-variant-prices.ts index abe7de74d5..0575d5aedd 100644 --- a/packages/workflows/src/handlers/product/upsert-variant-prices.ts +++ b/packages/workflows/src/handlers/product/upsert-variant-prices.ts @@ -23,11 +23,9 @@ type HandlerInput = { export async function upsertVariantPrices({ container, - context, data, }: WorkflowArguments) { const { variantPricesMap } = data - const featureFlagRouter = container.resolve("featureFlagRouter") if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { @@ -129,16 +127,16 @@ export async function upsertVariantPrices({ priceSetId = createdPriceSet?.id createdPriceSets.push(createdPriceSet) - } - linksToCreate.push({ - productService: { - variant_id: variantId, - }, - pricingService: { - price_set_id: priceSetId, - }, - }) + linksToCreate.push({ + productService: { + variant_id: variantId, + }, + pricingService: { + price_set_id: priceSetId, + }, + }) + } } const createdLinks = await remoteLink.create(linksToCreate)