From 148f537b47635e8b73ebaa27bbfbe58624bfe641 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:42:17 +0100 Subject: [PATCH] feat(medusa): integrate pricing module to core (#5304) * add pricing integraiton feature flag * init * first endpoint * cleanup * remove console.logs * refactor to util and implement across endpoints * add changeset * rename variables * remove mistype * feat(medusa): move price module integration to pricing service (#5322) * initial changes * chore: make product service always internal for pricing module * add notes --------- Co-authored-by: Riqwan Thamir * nit * cleanup * update to object querying * update cart integration test * remove uppercase currency_code * nit * Feat/admin product pricing module reads (#5354) * initial changes to list prices for admin * working price module implementation of list prices * nit * variant pricing * redo integration test changes * cleanup * cleanup * fix unit tests * [wip] Core <> Pricing - price updates (#5364) * chore: update medusa-app * wip * get links and modules working with migration * wip * chore: make test pass * Feat/rule type utils (#5371) * initial rule type utils * update migration script * chore: cleanup * ensure prices are always decorated * chore: use seed instead * chore: fix oas conflict * region id add to admin price read! --------- Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Co-authored-by: Philip Korsholm * pr feedback * create remoteQueryFunction type * fix merge * fix loaders issue * Feat(medusa, types, pricing): pricing module migration script (#5409) * add migration script for money amounts in pricing module * add changeset * rename file * cleanup imports * update changeset * add check for pricing module and ff * feat(medusa,workflows,types): update prices on product and variant update (#5412) * wip * chore: update product prices through workflow * chore: cleanup * chore: update product handler updates prices for variants * chore: handle reverts * chore: address pr comments * chore: scope workflow handlers to flag handlers * chore: update return * chore: update db url * chore: remove migration * chore: increase jest timeout * Feat(medusa): update migration and initDb to run link-migrations (#5437) * initial * loader update * more progress on loaders * update integration tests and remote-query loader * remove helper * migrate isolated modules * fix test * fix integration test * update with pr feedback * unregister medusa-app * re-register medusaApp * fix featureflag * set timeout * set timeout * conditionally run link-module migrations * pr feedback 1 * add driver options for db * throw if link is not defined in migration script * pass config module directly * include container in migrate command * chore: increase timeout * rm redis from api integration tests to test * chore: temporarily skip tests * chore: undo skips + add timeout for workflow tests * chore: increase timeout for order edits * re-add redis * include final resolution * add sharedcontainer to medusaapp loader * chore: move migration under run command * try removing redis_url from api tests * chore: cleanup server on process exit * chore: clear container on exit * chore: adjustments * chore: remove consoles * chore: close express app on finish * chore: destroy pg connection on shutdown * chore: skip * chore: unskip test * chore: cleanup container pg connection * chore: skip --------- Co-authored-by: Riqwan Thamir --- .changeset/dull-dolphins-cover.md | 7 + .changeset/silent-nails-chew.md | 5 + .../__tests__/admin/order-edit/order-edit.js | 2 +- .../api/__tests__/admin/product.js | 34 ++ integration-tests/api/medusa-config.js | 2 +- .../environment-helpers/bootstrap-app.js | 7 +- .../environment-helpers/setup-server.js | 26 +- .../environment-helpers/test-server.js | 11 +- .../environment-helpers/use-api.js | 11 + .../environment-helpers/use-container.js | 23 ++ .../environment-helpers/use-db.js | 65 +++- .../plugins/__tests__/cart/store/index.ts | 6 +- .../inventory-items/ff-many-to-many.js | 2 +- .../plugins/__tests__/pricing/get-product.ts | 139 +++++++++ .../__tests__/product/admin/create-product.ts | 115 +++++++ .../plugins/__tests__/product/admin/index.ts | 3 +- .../admin/update-product-variant.spec.ts | 253 +++++++++++++++ .../product/admin/update-product.spec.ts | 283 +++++++++++++++++ .../workflows/product/update-product.ts | 6 +- .../helpers/create-default-rule-types.ts | 14 + .../helpers/create-variant-price-set.ts | 51 +++ integration-tests/plugins/medusa-config.js | 11 + integration-tests/plugins/package.json | 2 + .../definitions/product-variant-price-set.ts | 4 +- packages/link-modules/src/initialize/index.ts | 6 +- .../admin/products/__tests__/get-product.js | 2 +- .../api/routes/admin/products/add-option.ts | 4 +- .../routes/admin/products/create-product.ts | 10 +- .../routes/admin/products/create-variant.ts | 21 +- .../routes/admin/products/delete-variant.ts | 2 +- .../api/routes/admin/products/get-product.ts | 7 +- .../routes/admin/products/list-products.ts | 4 +- .../api/routes/admin/products/set-metadata.ts | 6 +- .../routes/admin/products/update-option.ts | 8 +- .../routes/admin/products/update-product.ts | 4 +- .../routes/admin/products/update-variant.ts | 60 +++- .../api/routes/admin/variants/get-variant.ts | 2 +- .../routes/admin/variants/list-variants.ts | 2 +- .../api/routes/store/products/get-product.ts | 6 +- .../routes/store/products/list-products.ts | 4 +- .../api/routes/store/variants/get-variant.ts | 33 +- .../routes/store/variants/list-variants.ts | 39 ++- packages/medusa/src/commands/migrate.js | 44 ++- packages/medusa/src/commands/seed.ts | 44 ++- packages/medusa/src/index.js | 2 + .../feature-flags/isolate-pricing-domain.ts | 4 +- .../src/loaders/helpers/routing/index.ts | 20 +- packages/medusa/src/loaders/index.ts | 93 ++---- packages/medusa/src/loaders/medusa-app.ts | 67 ++++ packages/medusa/src/loaders/pg-connection.ts | 8 +- .../src/scripts/create-default-rule-types.ts | 44 +++ .../money-amount-pricing-module-migration.ts | 142 +++++++++ .../medusa/src/services/__mocks__/pricing.js | 6 + .../medusa/src/services/__tests__/cart.js | 29 +- packages/medusa/src/services/pricing.ts | 295 +++++++++++++++++- packages/medusa/src/types/pricing.ts | 11 + .../medusa/src/utils/merge-modules-config.ts | 36 +++ .../src/loaders/utils/load-internal.ts | 3 +- packages/modules-sdk/src/medusa-app.ts | 37 ++- packages/modules-sdk/src/medusa-module.ts | 23 ++ packages/modules-sdk/src/remote-query.ts | 16 +- .../pricing/src/services/pricing-module.ts | 49 ++- packages/types/src/modules-sdk/index.ts | 9 +- .../types/src/pricing/common/money-amount.ts | 4 +- .../pricing/common/price-set-money-amount.ts | 10 + packages/types/src/pricing/service.ts | 18 +- packages/types/src/workflow/product/index.ts | 1 + .../product/update-product-variants.ts | 41 +++ packages/utils/src/common/index.ts | 1 + packages/utils/src/common/remove-nullisih.ts | 14 + .../load-module-database-config.ts | 4 +- .../workflows/src/definition/product/index.ts | 1 + .../product/update-product-variants.ts | 108 +++++++ .../src/definition/product/update-products.ts | 44 ++- packages/workflows/src/definitions.ts | 6 + .../product/create-products-prepare-data.ts | 2 +- .../workflows/src/handlers/product/index.ts | 16 +- .../handlers/product/revert-variant-prices.ts | 48 +++ .../update-product-variants-prepare-data.ts | 95 ++++++ .../product/update-product-variants.ts | 39 +++ .../product/update-products-prepare-data.ts | 26 ++ .../update-products-variants-prices.ts | 50 ++- .../handlers/product/upsert-variant-prices.ts | 168 ++++++++++ yarn.lock | 6 +- 84 files changed, 2702 insertions(+), 284 deletions(-) create mode 100644 .changeset/dull-dolphins-cover.md create mode 100644 .changeset/silent-nails-chew.md create mode 100644 integration-tests/environment-helpers/use-container.js create mode 100644 integration-tests/plugins/__tests__/pricing/get-product.ts create mode 100644 integration-tests/plugins/__tests__/product/admin/create-product.ts create mode 100644 integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts create mode 100644 integration-tests/plugins/__tests__/product/admin/update-product.spec.ts create mode 100644 integration-tests/plugins/helpers/create-default-rule-types.ts create mode 100644 integration-tests/plugins/helpers/create-variant-price-set.ts create mode 100644 packages/medusa/src/loaders/medusa-app.ts create mode 100644 packages/medusa/src/scripts/create-default-rule-types.ts create mode 100644 packages/medusa/src/scripts/money-amount-pricing-module-migration.ts create mode 100644 packages/medusa/src/utils/merge-modules-config.ts create mode 100644 packages/types/src/workflow/product/update-product-variants.ts create mode 100644 packages/utils/src/common/remove-nullisih.ts create mode 100644 packages/workflows/src/definition/product/update-product-variants.ts create mode 100644 packages/workflows/src/handlers/product/revert-variant-prices.ts create mode 100644 packages/workflows/src/handlers/product/update-product-variants-prepare-data.ts create mode 100644 packages/workflows/src/handlers/product/update-product-variants.ts create mode 100644 packages/workflows/src/handlers/product/upsert-variant-prices.ts diff --git a/.changeset/dull-dolphins-cover.md b/.changeset/dull-dolphins-cover.md new file mode 100644 index 0000000000..0be8b0e617 --- /dev/null +++ b/.changeset/dull-dolphins-cover.md @@ -0,0 +1,7 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +"@medusajs/pricing": patch +--- + +feat(medusa): add migration script for pricing module diff --git a/.changeset/silent-nails-chew.md b/.changeset/silent-nails-chew.md new file mode 100644 index 0000000000..18a25eccab --- /dev/null +++ b/.changeset/silent-nails-chew.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): integrate pricing into reads diff --git a/integration-tests/api/__tests__/admin/order-edit/order-edit.js b/integration-tests/api/__tests__/admin/order-edit/order-edit.js index 729dfe70c8..a912262cb0 100644 --- a/integration-tests/api/__tests__/admin/order-edit/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit/order-edit.js @@ -21,7 +21,7 @@ const { } = require("../../../../factories") const setupServer = require("../../../../environment-helpers/setup-server") -jest.setTimeout(30000) +jest.setTimeout(100000) const adminHeaders = { headers: { diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 910e0749c6..d01d68bbe0 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -20,6 +20,7 @@ const { simpleDiscountFactory, simpleSalesChannelFactory, simpleRegionFactory, + simplePriceListFactory, } = require("../../../factories") const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist") const { IdMap } = require("medusa-test-utils") @@ -117,6 +118,39 @@ describe("/admin/products", () => { ) }) + it("should return prices not in price list for list product endpoint", async () => { + const api = useApi() + + await simplePriceListFactory(dbConnection, { + prices: [ + { + variant_id: "test-variant", + amount: 100, + currency_code: "usd", + }, + ], + }) + + const res = await api.get("/admin/products?id=test-product", adminHeaders) + + const prices = res.data.products[0].variants.map((v) => v.prices).flat() + + expect(res.status).toEqual(200) + expect(res.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product", + status: "draft", + }), + ]) + ) + expect(prices).toEqual( + expect.not.arrayContaining([ + expect.objectContaining({ price_list_id: expect.any(String) }), + ]) + ) + }) + it("returns a list of products where status is proposed", async () => { const api = useApi() diff --git a/integration-tests/api/medusa-config.js b/integration-tests/api/medusa-config.js index 527900350a..9720dd32c2 100644 --- a/integration-tests/api/medusa-config.js +++ b/integration-tests/api/medusa-config.js @@ -11,7 +11,7 @@ const enableResponseCompression = module.exports = { plugins: [], projectConfig: { - redis_url: redisUrl, + // redis_url: redisUrl, database_url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`, database_type: "postgres", jwt_secret: "test", diff --git a/integration-tests/environment-helpers/bootstrap-app.js b/integration-tests/environment-helpers/bootstrap-app.js index d87edf67d1..382e00aaff 100644 --- a/integration-tests/environment-helpers/bootstrap-app.js +++ b/integration-tests/environment-helpers/bootstrap-app.js @@ -1,11 +1,16 @@ const path = require("path") const express = require("express") const getPort = require("get-port") +const { isObject } = require("@medusajs/utils") module.exports = { - bootstrapApp: async ({ cwd } = {}) => { + bootstrapApp: async ({ cwd, env = {} } = {}) => { const app = express() + if (isObject(env)) { + Object.entries(env).forEach(([k, v]) => (process.env[k] = v)) + } + const loaders = require("@medusajs/medusa/dist/loaders").default const { container, dbConnection } = await loaders({ diff --git a/integration-tests/environment-helpers/setup-server.js b/integration-tests/environment-helpers/setup-server.js index 621064f1b4..17ac756522 100644 --- a/integration-tests/environment-helpers/setup-server.js +++ b/integration-tests/environment-helpers/setup-server.js @@ -1,10 +1,22 @@ const path = require("path") const { spawn } = require("child_process") -const { setPort } = require("./use-api") +const { setPort, useExpressServer } = require("./use-api") +const { setContainer } = require("./use-container") -module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => { +module.exports = ({ + cwd, + redisUrl, + uploadDir, + verbose, + env, + bootstrapApp = false, +}) => { const serverPath = path.join(__dirname, "test-server.js") + if (bootstrapApp) { + require(serverPath) + } + // in order to prevent conflicts in redis, use a different db for each worker // same fix as for databases (works with up to 15) // redis dbs are 0-indexed and jest worker ids are indexed from 1 @@ -44,5 +56,15 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => { setPort(port) resolve(medusaProcess) }) + + medusaProcess.on("exit", () => { + const expressServer = useExpressServer() + + setContainer(null) + + if (expressServer) { + expressServer.close() + } + }) }) } diff --git a/integration-tests/environment-helpers/test-server.js b/integration-tests/environment-helpers/test-server.js index 2282ab2127..8a049c032f 100644 --- a/integration-tests/environment-helpers/test-server.js +++ b/integration-tests/environment-helpers/test-server.js @@ -1,11 +1,18 @@ const { bootstrapApp } = require("./bootstrap-app") +const { setContainer } = require("./use-container") +const { setPort, setExpressServer } = require("./use-api") const setup = async () => { - const { app, port } = await bootstrapApp() + const { app, port, container } = await bootstrapApp() - app.listen(port, (err) => { + setContainer(container) + + const expressServer = app.listen(port, (err) => { + setPort(port) process.send(port) }) + + setExpressServer(expressServer) } setup() diff --git a/integration-tests/environment-helpers/use-api.js b/integration-tests/environment-helpers/use-api.js index e2cc75987a..e5c5237606 100644 --- a/integration-tests/environment-helpers/use-api.js +++ b/integration-tests/environment-helpers/use-api.js @@ -3,10 +3,15 @@ const axios = require("axios").default const ServerTestUtil = { port_: null, client_: null, + expressServer_: null, setPort: function (port) { this.client_ = axios.create({ baseURL: `http://localhost:${port}` }) }, + + setExpressServer: function (expressServer) { + this.expressServer_ = expressServer + }, } const instance = ServerTestUtil @@ -15,7 +20,13 @@ module.exports = { setPort: function (port) { instance.setPort(port) }, + setExpressServer: function (expressServer) { + instance.setExpressServer(expressServer) + }, useApi: function () { return instance.client_ }, + useExpressServer: function () { + return instance.expressServer_ + }, } diff --git a/integration-tests/environment-helpers/use-container.js b/integration-tests/environment-helpers/use-container.js new file mode 100644 index 0000000000..d1e66b1b14 --- /dev/null +++ b/integration-tests/environment-helpers/use-container.js @@ -0,0 +1,23 @@ +const path = require("path") +const express = require("express") +const getPort = require("get-port") +const { isObject } = require("@medusajs/utils") + +const AppUtils = { + container_: null, + + setContainer: function (container) { + this.container_ = container + }, +} + +const instance = AppUtils + +module.exports = { + setContainer: (container) => { + instance.setContainer(container) + }, + getContainer: () => { + return instance.container_ + }, +} diff --git a/integration-tests/environment-helpers/use-db.js b/integration-tests/environment-helpers/use-db.js index 18f40a2565..2fc418c430 100644 --- a/integration-tests/environment-helpers/use-db.js +++ b/integration-tests/environment-helpers/use-db.js @@ -1,9 +1,12 @@ const path = require("path") const { getConfigFile } = require("medusa-core-utils") +const { isObject, createMedusaContainer } = require("@medusajs/utils") const { dropDatabase } = require("pg-god") const { DataSource } = require("typeorm") const dbFactory = require("./use-template-db") +const { getContainer } = require("./use-container") +const { ContainerRegistrationKeys } = require("@medusajs/utils") const DB_HOST = process.env.DB_HOST const DB_USERNAME = process.env.DB_USERNAME @@ -29,20 +32,23 @@ const keepTables = [ const DbTestUtil = { db_: null, + pgConnection_: null, setDb: function (dataSource) { this.db_ = dataSource }, + setPgConnection: function (pgConnection) { + this.pgConnection_ = pgConnection + }, + clear: async function () { this.db_.synchronize(true) }, teardown: async function ({ forceDelete } = {}) { forceDelete = forceDelete || [] - const entities = this.db_.entityMetadatas - const manager = this.db_.manager await manager.query(`SET session_replication_role = 'replica';`) @@ -63,7 +69,15 @@ const DbTestUtil = { }, shutdown: async function () { + const container = getContainer() + const containerPgConnection = container.resolve( + ContainerRegistrationKeys.PG_CONNECTION + ) + await this.db_.destroy() + await this.pgConnection_?.context?.destroy() + await containerPgConnection?.context?.destroy() + return await dropDatabase({ DB_NAME }, pgGodCredentials) }, } @@ -71,14 +85,17 @@ const DbTestUtil = { const instance = DbTestUtil module.exports = { - initDb: async function ({ cwd, database_extra }) { + initDb: async function ({ cwd, database_extra, env }) { + if (isObject(env)) { + Object.entries(env).forEach(([k, v]) => (process.env[k] = v)) + } + const { configModule } = getConfigFile(cwd, `medusa-config`) - const { featureFlags } = configModule const featureFlagsLoader = require("@medusajs/medusa/dist/loaders/feature-flags").default - const featureFlagsRouter = featureFlagsLoader({ featureFlags }) + const featureFlagRouter = featureFlagsLoader(configModule) const modelsLoader = require("@medusajs/medusa/dist/loaders/models").default const entities = modelsLoader({}, { register: false }) @@ -104,10 +121,10 @@ module.exports = { } = require("@medusajs/medusa/dist/commands/utils/get-migrations") const { migrations: moduleMigrations, models: moduleModels } = - getModuleSharedResources(configModule, featureFlagsRouter) + getModuleSharedResources(configModule, featureFlagRouter) const enabledMigrations = getEnabledMigrations([migrationDir], (flag) => - featureFlagsRouter.isFeatureEnabled(flag) + featureFlagRouter.isFeatureEnabled(flag) ) const enabledEntities = entities.filter( @@ -128,6 +145,40 @@ module.exports = { await dbDataSource.runMigrations() instance.setDb(dbDataSource) + + const IsolateProductDomainFeatureFlag = + require("@medusajs/medusa/dist/loaders/feature-flags/isolate-product-domain").default + const IsolatePricingDomainFeatureFlag = + require("@medusajs/medusa/dist/loaders/feature-flags/isolate-pricing-domain").default + + if ( + featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) || + featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key) + ) { + const pgConnectionLoader = + require("@medusajs/medusa/dist/loaders/pg-connection").default + + const medusaAppLoader = + require("@medusajs/medusa/dist/loaders/medusa-app").default + + const container = createMedusaContainer() + + const pgConnection = await pgConnectionLoader({ configModule, container }) + instance.setPgConnection(pgConnection) + + const { runMigrations } = await medusaAppLoader( + { configModule, container }, + { registerInContainer: false } + ) + + const options = { + database: { + clientUrl: DB_URL, + }, + } + await runMigrations(options) + } + return dbDataSource }, useDb: function () { diff --git a/integration-tests/plugins/__tests__/cart/store/index.ts b/integration-tests/plugins/__tests__/cart/store/index.ts index 14eae47236..75544ca654 100644 --- a/integration-tests/plugins/__tests__/cart/store/index.ts +++ b/integration-tests/plugins/__tests__/cart/store/index.ts @@ -1,12 +1,12 @@ import { MoneyAmount, PriceList, Region } from "@medusajs/medusa" import path from "path" +import { ProductVariantMoneyAmount } from "@medusajs/medusa" import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app" import setupServer from "../../../../environment-helpers/setup-server" import { setPort, useApi } from "../../../../environment-helpers/use-api" import { initDb, useDb } from "../../../../environment-helpers/use-db" import { simpleProductFactory } from "../../../../factories" -import { ProductVariantMoneyAmount } from "@medusajs/medusa" jest.setTimeout(30000) @@ -23,7 +23,7 @@ describe("/store/carts", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd, verbose: true }) + medusaProcess = await setupServer({ cwd }) const { app, port } = await bootstrapApp({ cwd }) setPort(port) express = app.listen(port, () => { @@ -134,7 +134,7 @@ describe("/store/carts", () => { await dbConnection.manager.save(ma_sale_1) await dbConnection.manager.insert(ProductVariantMoneyAmount, { - id: 'pvma-test', + id: "pvma-test", variant_id: prodSale.variants[0].id, money_amount_id: ma_sale_1.id, }) diff --git a/integration-tests/plugins/__tests__/inventory/inventory-items/ff-many-to-many.js b/integration-tests/plugins/__tests__/inventory/inventory-items/ff-many-to-many.js index 9e7c136b96..bd6f58f87f 100644 --- a/integration-tests/plugins/__tests__/inventory/inventory-items/ff-many-to-many.js +++ b/integration-tests/plugins/__tests__/inventory/inventory-items/ff-many-to-many.js @@ -32,7 +32,7 @@ describe("Inventory Items endpoints", () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) dbConnection = await initDb({ cwd }) - const { container, app, port } = await bootstrapApp({ cwd, verbose: true }) + const { container, app, port } = await bootstrapApp({ cwd }) appContainer = container // Set feature flag diff --git a/integration-tests/plugins/__tests__/pricing/get-product.ts b/integration-tests/plugins/__tests__/pricing/get-product.ts new file mode 100644 index 0000000000..6591fa8946 --- /dev/null +++ b/integration-tests/plugins/__tests__/pricing/get-product.ts @@ -0,0 +1,139 @@ +import { setPort, useApi } from "../../../environment-helpers/use-api" +import { initDb, useDb } from "../../../environment-helpers/use-db" +import { simpleCartFactory, simpleRegionFactory } from "../../../factories" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { AxiosInstance } from "axios" +import path from "path" +import { bootstrapApp } from "../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../helpers/admin-seeder" + +jest.setTimeout(5000000) + +const DB_HOST = process.env.DB_HOST +const DB_USERNAME = process.env.DB_USERNAME +const DB_PASSWORD = process.env.DB_PASSWORD +const DB_NAME = process.env.DB_TEMP_NAME +const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}` + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_ISOLATE_PRICING_DOMAIN: true, + MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN: true, +} + +describe("Link Modules", () => { + let medusaContainer + let dbConnection + let express + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + + const { container, app, port } = await bootstrapApp({ cwd, env }) + medusaContainer = container + setPort(port) + + express = app.listen(port) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + express.close() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await simpleRegionFactory(dbConnection, { + id: "region-1", + currency_code: "usd", + }) + }) + + describe("get product price", () => { + let ruleType + let priceSet + let productId + const cartId = "test-cart" + beforeEach(async () => { + const pricingModuleService = medusaContainer.resolve( + ModuleRegistrationName.PRICING + ) + const api = useApi()! as AxiosInstance + + await simpleCartFactory(dbConnection, { id: cartId, region: "region-1" }) + + const payload = { + title: "Test", + description: "test-product-description", + images: ["test-image.png", "test-image-2.png"], + variants: [ + { + title: "Test variant", + prices: [], + options: [], + }, + ], + } + + const response = await api.post("/admin/products", payload, adminHeaders) + + productId = response.data.product.id + const variant = response.data.product.variants[0] + + ruleType = await pricingModuleService.createRuleTypes([ + { name: "region_id", rule_attribute: "region_id" }, + ]) + + priceSet = await pricingModuleService.create({ + rules: [{ rule_attribute: "region_id" }], + prices: [ + { + amount: 1000, + currency_code: "usd", + rules: { region_id: "region-1" }, + }, + { + amount: 900, + currency_code: "usd", + rules: { region_id: "region-2" }, + }, + ], + }) + + const remoteLink = medusaContainer.resolve("remoteLink") as any + + await remoteLink.create({ + productService: { + variant_id: variant.id, + }, + pricingService: { + price_set_id: priceSet.id, + }, + }) + }) + + it("Should get prices declared in pricing module", async () => { + const api = useApi()! as AxiosInstance + + const response = await api.get( + `/store/products/${productId}?cart_id=${cartId}` + ) + + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + amount: 1000, + currency_code: "usd", + }), + ]) + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/product/admin/create-product.ts b/integration-tests/plugins/__tests__/product/admin/create-product.ts new file mode 100644 index 0000000000..1cd5207ad3 --- /dev/null +++ b/integration-tests/plugins/__tests__/product/admin/create-product.ts @@ -0,0 +1,115 @@ +import { initDb, useDb } from "../../../../environment-helpers/use-db" + +import { Region } from "@medusajs/medusa" +import { AxiosInstance } from "axios" +import path from "path" +import setupServer from "../../../../environment-helpers/setup-server" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +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_ISOLATE_PRICING_DOMAIN: true, + MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN: true, +} + +describe("[Product & Pricing Module] POST /admin/products", () => { + let dbConnection + let appContainer + let medusaProcess + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + medusaProcess = await setupServer({ cwd, env, bootstrapApp: true } as any) + appContainer = getContainer() + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) + + beforeEach(async () => { + const manager = dbConnection.manager + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await manager.insert(Region, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create prices with region_id and currency_code context", async () => { + const api = useApi()! as AxiosInstance + + const data = { + title: "test product", + options: [{ title: "test-option" }], + variants: [ + { + title: "test variant", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + }, + ], + options: [{ value: "test-option" }], + }, + ], + } + + let response = await api.post( + "/admin/products?relations=variants.prices", + data, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data).toEqual({ + product: expect.objectContaining({ + id: expect.any(String), + title: "test product", + variants: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + title: "test variant", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + }), + expect.objectContaining({ + amount: 55500, + currency_code: "usd", + }), + ]), + }), + ]), + }), + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/product/admin/index.ts b/integration-tests/plugins/__tests__/product/admin/index.ts index 53567b11c5..ffca3acb7f 100644 --- a/integration-tests/plugins/__tests__/product/admin/index.ts +++ b/integration-tests/plugins/__tests__/product/admin/index.ts @@ -23,7 +23,6 @@ const adminHeaders = { } describe("/admin/products", () => { - let medusaProcess let dbConnection let express let medusaContainer @@ -44,7 +43,7 @@ describe("/admin/products", () => { const db = useDb() await db.shutdown() - medusaProcess.kill() + express.close() }) it("Should have loaded the product module", function () { 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 new file mode 100644 index 0000000000..69387aa48d --- /dev/null +++ b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts @@ -0,0 +1,253 @@ +import setupServer from "../../../../environment-helpers/setup-server" +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 path from "path" +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_ISOLATE_PRICING_DOMAIN: true, + MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN: true, +} + +describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", () => { + let dbConnection + let appContainer + let medusaProcess + let product + let variant + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + medusaProcess = await setupServer({ cwd, env, bootstrapApp: true } as any) + appContainer = getContainer() + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) + + 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 product variant price sets and prices", async () => { + const api = useApi() + const data = { + title: "test variant update", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + region_id: null, + }, + ], + } + + let response = await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + response = await api.get(`/admin/products/${product.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + title: "test variant update", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + amount: 55500, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) + + it("should update money amounts if money amount id is present in prices", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const moneyAmountToUpdate = priceSet.money_amounts?.[0] + + const api = useApi() + const data = { + title: "test variant update", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + id: moneyAmountToUpdate?.id, + amount: 2222, + currency_code: "usd", + region_id: null, + }, + ], + } + + let response = await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + response = await api.get(`/admin/products/${product.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + title: "test variant update", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + id: moneyAmountToUpdate?.id, + amount: 2222, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) + + it("should add prices if price set is already present", async () => { + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [], + }) + + const api = useApi() + const data = { + title: "test variant update", + prices: [ + { + amount: 123, + region_id: "test-region", + }, + { + amount: 456, + currency_code: "usd", + region_id: null, + }, + ], + } + + let response = await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + response = await api.get(`/admin/products/${product.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + title: "test variant update", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 123, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + amount: 456, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts b/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts new file mode 100644 index 0000000000..f095368871 --- /dev/null +++ b/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts @@ -0,0 +1,283 @@ +import setupServer from "../../../../environment-helpers/setup-server" +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 { Region } from "@medusajs/medusa" +import { AxiosInstance } from "axios" +import path from "path" +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_ISOLATE_PRICING_DOMAIN: true, + MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN: true, +} + +describe.skip("[Product & Pricing Module] POST /admin/products/:id", () => { + let dbConnection + let appContainer + let medusaProcess + let product + let variant + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + medusaProcess = await setupServer({ cwd, env, bootstrapApp: true } as any) + appContainer = getContainer() + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + medusaProcess.kill() + }) + + beforeEach(async () => { + const manager = dbConnection.manager + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await manager.insert(Region, { + 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 product variant price sets and prices", async () => { + const api = useApi() + const data = { + title: "test product update", + variants: [ + { + id: variant.id, + title: "test variant update", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + region_id: null, + }, + ], + }, + ], + } + + let response = await api.post( + `/admin/products/${product.id}`, + data, + adminHeaders + ) + + response = await api.get(`/admin/products/${product.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + title: "test variant update", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + amount: 55500, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) + + it("should update money amounts if money amount id is present in prices", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const moneyAmountToUpdate = priceSet.money_amounts?.[0] + + const api = useApi() + const data = { + title: "test product update", + variants: [ + { + id: variant.id, + title: "test variant update", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + id: moneyAmountToUpdate?.id, + amount: 2222, + currency_code: "usd", + region_id: null, + }, + ], + }, + ], + } + + let response = await api.post( + `/admin/products/${product.id}`, + data, + adminHeaders + ) + + response = await api.get(`/admin/products/${product.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + title: "test variant update", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + id: moneyAmountToUpdate?.id, + amount: 2222, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) + + it("should add prices if price set is already present", async () => { + const remoteLink = appContainer.resolve("remoteLink") + const pricingModuleService = appContainer.resolve("pricingModuleService") + + const priceSet = await pricingModuleService.create({ + rules: [{ rule_attribute: "region_id" }], + prices: [], + }) + + await remoteLink.create({ + productService: { + variant_id: variant.id, + }, + pricingService: { + price_set_id: priceSet.id, + }, + }) + + const api = useApi()! as AxiosInstance + + const data = { + title: "test product update", + variants: [ + { + id: variant.id, + title: "test variant update", + prices: [ + { + amount: 123, + region_id: "test-region", + }, + { + amount: 456, + currency_code: "usd", + region_id: null, + }, + ], + }, + ], + } + + let response = await api.post( + `/admin/products/${product.id}`, + data, + adminHeaders + ) + + response = await api.get(`/admin/products/${product.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + title: "test variant update", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 123, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + amount: 456, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/workflows/product/update-product.ts b/integration-tests/plugins/__tests__/workflows/product/update-product.ts index 9f08dcbdd0..38d9772166 100644 --- a/integration-tests/plugins/__tests__/workflows/product/update-product.ts +++ b/integration-tests/plugins/__tests__/workflows/product/update-product.ts @@ -1,16 +1,18 @@ +import { WorkflowTypes } from "@medusajs/types" import { Handlers, pipe, updateProducts, UpdateProductsActions, } from "@medusajs/workflows" -import { WorkflowTypes } from "@medusajs/types" import path from "path" -import { initDb, useDb } from "../../../../environment-helpers/use-db" import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { initDb, useDb } from "../../../../environment-helpers/use-db" import { simpleProductFactory } from "../../../../factories" +jest.setTimeout(30000) + describe("UpdateProduct workflow", function () { let medusaProcess let dbConnection diff --git a/integration-tests/plugins/helpers/create-default-rule-types.ts b/integration-tests/plugins/helpers/create-default-rule-types.ts new file mode 100644 index 0000000000..e063d642bb --- /dev/null +++ b/integration-tests/plugins/helpers/create-default-rule-types.ts @@ -0,0 +1,14 @@ +import { IPricingModuleService } from "@medusajs/types" + +export const createDefaultRuleTypes = async (container) => { + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + + return pricingModuleService.createRuleTypes([ + { + name: "region_id", + rule_attribute: "region_id", + }, + ]) +} diff --git a/integration-tests/plugins/helpers/create-variant-price-set.ts b/integration-tests/plugins/helpers/create-variant-price-set.ts new file mode 100644 index 0000000000..1ea28b1c56 --- /dev/null +++ b/integration-tests/plugins/helpers/create-variant-price-set.ts @@ -0,0 +1,51 @@ +import { MedusaContainer } from "@medusajs/modules-sdk" +import { + CreatePriceSetDTO, + IPricingModuleService, + PriceSetDTO, +} from "@medusajs/types" + +const defaultPrices = [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, +] + +const defaultPriceSetRules = [{ rule_attribute: "region_id" }] + +export const createVariantPriceSet = async ({ + container, + variantId, + prices = defaultPrices, + rules = defaultPriceSetRules, +}: { + container: MedusaContainer + variantId: string + prices?: CreatePriceSetDTO["prices"] + rules?: CreatePriceSetDTO["rules"] +}): Promise => { + const remoteLink = container.resolve("remoteLink") + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + + const priceSet = await pricingModuleService.create({ + rules, + prices, + }) + + await remoteLink.create({ + productService: { + variant_id: variantId, + }, + pricingService: { + price_set_id: priceSet.id, + }, + }) + + return await pricingModuleService.retrieve(priceSet.id, { + relations: ["money_amounts"], + }) +} diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index 7f76569523..fd215f04e2 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -5,6 +5,10 @@ const DB_USERNAME = process.env.DB_USERNAME const DB_PASSWORD = process.env.DB_PASSWORD const DB_NAME = process.env.DB_TEMP_NAME const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}` +process.env.POSTGRES_URL = DB_URL + +const enablePricing = process.env.MEDUSA_FF_ISOLATE_PRICING_DOMAIN == "true" +const enableProduct = process.env.MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN == "true" module.exports = { plugins: [ @@ -33,6 +37,8 @@ module.exports = { database_extra: { idle_in_transaction_session_timeout: 0 }, }, featureFlags: { + isolate_product_domain: enableProduct, + isolate_pricing_domain: enablePricing, workflows: { [Workflows.CreateProducts]: true, [Workflows.UpdateProducts]: true, @@ -59,5 +65,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/product", }, + [Modules.PRICING]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/pricing", + }, }, } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 33f4d43275..a5f73b935c 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -13,6 +13,8 @@ "@medusajs/event-bus-local": "workspace:*", "@medusajs/inventory": "workspace:^", "@medusajs/medusa": "workspace:*", + "@medusajs/modules-sdk": "workspace:^", + "@medusajs/pricing": "workspace:^", "@medusajs/product": "workspace:^", "faker": "^5.5.3", "medusa-fulfillment-webshipper": "workspace:*", diff --git a/packages/link-modules/src/definitions/product-variant-price-set.ts b/packages/link-modules/src/definitions/product-variant-price-set.ts index b95e1d6e0b..0da36ab55a 100644 --- a/packages/link-modules/src/definitions/product-variant-price-set.ts +++ b/packages/link-modules/src/definitions/product-variant-price-set.ts @@ -1,6 +1,6 @@ -import { Modules } from "@medusajs/modules-sdk" -import { ModuleJoinerConfig } from "@medusajs/types" import { LINKS } from "../links" +import { ModuleJoinerConfig } from "@medusajs/types" +import { Modules } from "@medusajs/modules-sdk" export const ProductVariantPriceSet: ModuleJoinerConfig = { serviceName: LINKS.ProductVariantPriceSet, diff --git a/packages/link-modules/src/initialize/index.ts b/packages/link-modules/src/initialize/index.ts index 675bd09973..a020f555bc 100644 --- a/packages/link-modules/src/initialize/index.ts +++ b/packages/link-modules/src/initialize/index.ts @@ -178,10 +178,8 @@ export async function runMigrations( allLinks.add(serviceKey) if ( - (!primary.isInternalService && - !modulesLoadedKeys.includes(primary.serviceName)) || - (!foreign.isInternalService && - !modulesLoadedKeys.includes(foreign.serviceName)) + !modulesLoadedKeys.includes(primary.serviceName) || + !modulesLoadedKeys.includes(foreign.serviceName) ) { continue } diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js index 9f22e5fdbb..f99ea63f7e 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js @@ -1,6 +1,6 @@ import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" import { ProductServiceMock } from "../../../../../services/__mocks__/product" +import { request } from "../../../../../helpers/test-request" describe("GET /admin/products/:id", () => { describe("successfully gets a product", () => { diff --git a/packages/medusa/src/api/routes/admin/products/add-option.ts b/packages/medusa/src/api/routes/admin/products/add-option.ts index 91ba874a6e..3ababaadff 100644 --- a/packages/medusa/src/api/routes/admin/products/add-option.ts +++ b/packages/medusa/src/api/routes/admin/products/add-option.ts @@ -1,9 +1,9 @@ import { PricingService, ProductService } from "../../../../services" import { defaultAdminProductFields, defaultAdminProductRelations } from "." +import { EntityManager } from "typeorm" import { IsString } from "class-validator" import { validator } from "../../../../utils/validator" -import { EntityManager } from "typeorm" /** * @oas [post] /admin/products/{id}/options @@ -91,7 +91,7 @@ export default async (req, res) => { relations: defaultAdminProductRelations, }) - const [product] = await pricingService.setProductPrices([rawProduct]) + const [product] = await pricingService.setAdminProductPricing([rawProduct]) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index e0bb2216c4..26b5e993b2 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -1,3 +1,5 @@ +import { IInventoryService, WorkflowTypes } from "@medusajs/types" +import { Workflows, createProducts } from "@medusajs/workflows" import { IsArray, IsBoolean, @@ -37,17 +39,15 @@ import { } from "./transaction/create-product-variant" import { DistributedTransaction } from "@medusajs/orchestration" -import { IInventoryService, WorkflowTypes } from "@medusajs/types" import { FlagRouter } from "@medusajs/utils" -import { createProducts, Workflows } from "@medusajs/workflows" import { Type } from "class-transformer" import { EntityManager } from "typeorm" +import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" import { ProductStatus } from "../../../../models" import { Logger } from "../../../../types/global" import { validator } from "../../../../utils" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" -import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" /** * @oas [post] /admin/products @@ -269,7 +269,9 @@ export default async (req, res) => { }) } - const [pricedProduct] = await pricingService.setProductPrices([rawProduct]) + const [pricedProduct] = await pricingService.setAdminProductPricing([ + rawProduct, + ]) res.json({ product: pricedProduct }) } 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 a69d0d7d3b..ffddb375fb 100644 --- a/packages/medusa/src/api/routes/admin/products/create-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/create-variant.ts @@ -1,5 +1,7 @@ -import { IInventoryService } from "@medusajs/types" -import { Type } from "class-transformer" +import { + CreateProductVariantInput, + ProductVariantPricesCreateReq, +} from "../../../../types/product-variant" import { IsArray, IsBoolean, @@ -9,20 +11,19 @@ import { IsString, ValidateNested, } from "class-validator" -import { EntityManager } from "typeorm" -import { defaultAdminProductFields, defaultAdminProductRelations } from "." import { PricingService, ProductService, ProductVariantInventoryService, ProductVariantService, } from "../../../../services" -import { - CreateProductVariantInput, - ProductVariantPricesCreateReq, -} from "../../../../types/product-variant" -import { validator } from "../../../../utils/validator" +import { defaultAdminProductFields, defaultAdminProductRelations } from "." + +import { EntityManager } from "typeorm" +import { IInventoryService } from "@medusajs/types" +import { Type } from "class-transformer" import { createVariantsTransaction } from "./transaction/create-product-variant" +import { validator } from "../../../../utils/validator" /** * @oas [post] /admin/products/{id}/variants @@ -152,7 +153,7 @@ export default async (req, res) => { relations: defaultAdminProductRelations, }) - const [product] = await pricingService.setProductPrices([rawProduct]) + const [product] = await pricingService.setAdminProductPricing([rawProduct]) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/delete-variant.ts b/packages/medusa/src/api/routes/admin/products/delete-variant.ts index ab6d398ae9..f3f42ad99c 100644 --- a/packages/medusa/src/api/routes/admin/products/delete-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/delete-variant.ts @@ -81,7 +81,7 @@ export default async (req, res) => { relations: defaultAdminProductRelations, }) - const [product] = await pricingService.setProductPrices([data]) + const [product] = await pricingService.setAdminProductPricing([data]) res.json({ variant_id, 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 ca5db86615..c7d636b274 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.ts +++ b/packages/medusa/src/api/routes/admin/products/get-product.ts @@ -1,7 +1,8 @@ import { PricingService, ProductService } from "../../../../services" + import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" -import { defaultAdminProductRemoteQueryObject } from "./index" import { MedusaError } from "@medusajs/utils" +import { defaultAdminProductRemoteQueryObject } from "./index" /** * @oas [get] /admin/products/{id} @@ -80,8 +81,8 @@ export default async (req, res) => { const product = rawProduct - if (!shouldSetPricing) { - await pricingService.setProductPrices([product]) + if (shouldSetPricing) { + await pricingService.setAdminProductPricing([product]) } res.json({ product }) 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 8bb3eb8c85..8554298064 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -9,10 +9,10 @@ import { import { FilterableProductProps } from "../../../../types/product" import { IInventoryService } from "@medusajs/types" +import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" import { PricedProduct } from "../../../../types/pricing" import { Product } from "../../../../models" import { Type } from "class-transformer" -import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" import { defaultAdminProductRemoteQueryObject } from "./index" /** @@ -275,7 +275,7 @@ export default async (req, res) => { ) if (shouldSetPricing) { - products = await pricingService.setProductPrices(rawProducts) + products = await pricingService.setAdminProductPricing(rawProducts) } // We only set availability if variants are requested diff --git a/packages/medusa/src/api/routes/admin/products/set-metadata.ts b/packages/medusa/src/api/routes/admin/products/set-metadata.ts index 8c47e9c075..641de61345 100644 --- a/packages/medusa/src/api/routes/admin/products/set-metadata.ts +++ b/packages/medusa/src/api/routes/admin/products/set-metadata.ts @@ -1,9 +1,9 @@ import { defaultAdminProductFields, defaultAdminProductRelations } from "." -import { IsString } from "class-validator" -import { validator } from "../../../../utils/validator" import { EntityManager } from "typeorm" +import { IsString } from "class-validator" import { PricingService } from "../../../../services" +import { validator } from "../../../../utils/validator" /** * @oas [post] /admin/products/{id}/metadata @@ -96,7 +96,7 @@ export default async (req, res) => { relations: defaultAdminProductRelations, }) - const [product] = await pricingService.setProductPrices([rawProduct]) + const [product] = await pricingService.setAdminProductPricing([rawProduct]) res.status(200).json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/update-option.ts b/packages/medusa/src/api/routes/admin/products/update-option.ts index 278e62beff..08277e0c0c 100644 --- a/packages/medusa/src/api/routes/admin/products/update-option.ts +++ b/packages/medusa/src/api/routes/admin/products/update-option.ts @@ -1,9 +1,9 @@ +import { PricingService, ProductService } from "../../../../services" import { defaultAdminProductFields, defaultAdminProductRelations } from "." -import { IsString } from "class-validator" -import { PricingService, ProductService } from "../../../../services" -import { validator } from "../../../../utils/validator" import { EntityManager } from "typeorm" +import { IsString } from "class-validator" +import { validator } from "../../../../utils/validator" /** * @oas [post] /admin/products/{id}/options/{option_id} @@ -92,7 +92,7 @@ export default async (req, res) => { relations: defaultAdminProductRelations, }) - const [product] = await pricingService.setProductPrices([rawProduct]) + const [product] = await pricingService.setAdminProductPricing([rawProduct]) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/update-product.ts b/packages/medusa/src/api/routes/admin/products/update-product.ts index ea52c2da52..e25abc5788 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.ts +++ b/packages/medusa/src/api/routes/admin/products/update-product.ts @@ -137,7 +137,6 @@ export default async (req, res) => { req.scope.resolve("inventoryService") const manager: EntityManager = req.scope.resolve("manager") - const productModuleService = req.scope.resolve("productModuleService") const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") @@ -211,6 +210,7 @@ export default async (req, res) => { prices: variant.prices || [], }) variantsToCreate.push(variant) + continue } @@ -298,7 +298,7 @@ export default async (req, res) => { }) } - const [product] = await pricingService.setProductPrices([rawProduct]) + const [product] = await pricingService.setAdminProductPricing([rawProduct]) res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.ts b/packages/medusa/src/api/routes/admin/products/update-variant.ts index 0782cfae7b..a8e4c8ea93 100644 --- a/packages/medusa/src/api/routes/admin/products/update-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/update-variant.ts @@ -1,3 +1,7 @@ +import { WorkflowTypes } from "@medusajs/types" +import { FlagRouter } from "@medusajs/utils" +import { UpdateProductVariants } from "@medusajs/workflows" +import { Type } from "class-transformer" import { IsArray, IsBoolean, @@ -7,15 +11,15 @@ import { IsString, ValidateNested, } from "class-validator" +import { EntityManager } from "typeorm" + import { defaultAdminProductFields, defaultAdminProductRelations } from "." +import IsolatePricingDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-pricing-domain" import { PricingService, ProductService, ProductVariantService, } from "../../../../services" - -import { Type } from "class-transformer" -import { EntityManager } from "typeorm" import { PriceSelectionParams } from "../../../../types/price-selection" import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant" import { validator } from "../../../../utils/validator" @@ -106,6 +110,13 @@ import { validator } from "../../../../utils/validator" export default async (req, res) => { const { id, variant_id } = req.params + const manager: EntityManager = req.scope.resolve("manager") + const productService: ProductService = req.scope.resolve("productService") + const pricingService: PricingService = req.scope.resolve("pricingService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + const productVariantService: ProductVariantService = req.scope.resolve( + "productVariantService" + ) const validated = await validator( AdminPostProductsProductVariantsVariantReq, req.body @@ -113,21 +124,36 @@ export default async (req, res) => { const validatedQueryParams = await validator(PriceSelectionParams, req.query) - const productService: ProductService = req.scope.resolve("productService") - const pricingService: PricingService = req.scope.resolve("pricingService") - const productVariantService: ProductVariantService = req.scope.resolve( - "productVariantService" - ) + if (featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key)) { + const updateVariantsWorkflow = UpdateProductVariants.updateProductVariants( + req.scope + ) - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - await productVariantService - .withTransaction(transactionManager) - .update(variant_id, { - product_id: id, - ...validated, - }) - }) + const input = { + productVariants: [ + { + id: variant_id, + ...validated, + }, + ] as WorkflowTypes.ProductWorkflow.UpdateProductVariantsInputDTO[], + } + + await updateVariantsWorkflow.run({ + input, + context: { + manager, + }, + }) + } else { + await manager.transaction(async (transactionManager) => { + await productVariantService + .withTransaction(transactionManager) + .update(variant_id, { + product_id: id, + ...validated, + }) + }) + } const rawProduct = await productService.retrieve(id, { select: defaultAdminProductFields, diff --git a/packages/medusa/src/api/routes/admin/variants/get-variant.ts b/packages/medusa/src/api/routes/admin/variants/get-variant.ts index 12d8530a57..1826d9f23b 100644 --- a/packages/medusa/src/api/routes/admin/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/admin/variants/get-variant.ts @@ -70,7 +70,7 @@ export default async (req, res) => { req.retrieveConfig ) - const [variant] = await pricingService.setVariantPrices([rawVariant]) + const [variant] = await pricingService.setAdminVariantPricing([rawVariant]) res.status(200).json({ variant }) } diff --git a/packages/medusa/src/api/routes/admin/variants/list-variants.ts b/packages/medusa/src/api/routes/admin/variants/list-variants.ts index 304b44ccd9..73a7c035b0 100644 --- a/packages/medusa/src/api/routes/admin/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/admin/variants/list-variants.ts @@ -192,7 +192,7 @@ export default async (req, res) => { currencyCode = region.currency_code } - let variants = await pricingService.setVariantPrices(rawVariants, { + let variants = await pricingService.setAdminVariantPricing(rawVariants, { cart_id: req.validatedQuery.cart_id, region_id: regionId, currency_code: currencyCode, diff --git a/packages/medusa/src/api/routes/store/products/get-product.ts b/packages/medusa/src/api/routes/store/products/get-product.ts index 18f136fa85..50cef80939 100644 --- a/packages/medusa/src/api/routes/store/products/get-product.ts +++ b/packages/medusa/src/api/routes/store/products/get-product.ts @@ -1,3 +1,4 @@ +import { IsOptional, IsString } from "class-validator" import { CartService, PricingService, @@ -5,13 +6,12 @@ import { ProductVariantInventoryService, RegionService, } from "../../../../services" -import { IsOptional, IsString } from "class-validator" +import { MedusaError } from "@medusajs/utils" +import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain" import { PriceSelectionParams } from "../../../../types/price-selection" import { cleanResponseData } from "../../../../utils" -import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain" import { defaultStoreProductRemoteQueryObject } from "./index" -import { MedusaError } from "@medusajs/utils" /** * @oas [get] /store/products/{id} diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index 5c40b30307..d7f7928044 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -17,14 +17,14 @@ import { Transform, Type } from "class-transformer" import { DateComparisonOperator } from "../../../../types/common" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { IsType } from "../../../../utils/validators/is-type" +import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain" import { PriceSelectionParams } from "../../../../types/price-selection" import PricingService from "../../../../services/pricing" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" import { cleanResponseData } from "../../../../utils/clean-response-data" import { defaultStoreCategoryScope } from "../product-categories" -import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" -import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain" import { defaultStoreProductRemoteQueryObject } from "./index" +import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" /** * @oas [get] /store/products diff --git a/packages/medusa/src/api/routes/store/variants/get-variant.ts b/packages/medusa/src/api/routes/store/variants/get-variant.ts index d440b2a9db..afc86bcb4c 100644 --- a/packages/medusa/src/api/routes/store/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/store/variants/get-variant.ts @@ -7,9 +7,7 @@ import { } from "../../../../services" import { IsOptional, IsString } from "class-validator" -import { FindParams } from "../../../../types/common" import { PriceSelectionParams } from "../../../../types/price-selection" -import { defaultStoreVariantRelations } from "." import { validator } from "../../../../utils/validator" /** @@ -81,10 +79,11 @@ export default async (req, res) => { req.scope.resolve("productVariantInventoryService") const cartService: CartService = req.scope.resolve("cartService") const regionService: RegionService = req.scope.resolve("regionService") + const featureFlagRouter = req.scope.resolve("featureFlagRouter") const customer_id = req.user?.customer_id - const rawVariant = await variantService.retrieve(id, req.retrieveConfig) + const variant = await variantService.retrieve(id, req.retrieveConfig) let sales_channel_id = validated.sales_channel_id if (req.publishableApiKeyScopes?.sales_channel_ids.length === 1) { @@ -104,19 +103,27 @@ export default async (req, res) => { currencyCode = region.currency_code } - const variantRes = await pricingService.setVariantPrices([rawVariant], { - cart_id: validated.cart_id, - customer_id: customer_id, - region_id: regionId, - currency_code: currencyCode, - include_discount_prices: true, - }) + const decoratePromises: Promise[] = [] - const [variant] = await productVariantInventoryService.setVariantAvailability( - variantRes, - sales_channel_id + decoratePromises.push( + (await pricingService.setVariantPrices([variant], { + cart_id: validated.cart_id, + customer_id: customer_id, + region_id: regionId, + currency_code: currencyCode, + include_discount_prices: true, + })) as any ) + decoratePromises.push( + (await productVariantInventoryService.setVariantAvailability( + [variant], + sales_channel_id + )) as any + ) + + await Promise.all(decoratePromises) + res.json({ variant }) } diff --git a/packages/medusa/src/api/routes/store/variants/list-variants.ts b/packages/medusa/src/api/routes/store/variants/list-variants.ts index cc5e2bc92d..34417b6a83 100644 --- a/packages/medusa/src/api/routes/store/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/store/variants/list-variants.ts @@ -1,4 +1,3 @@ -import { IsInt, IsOptional, IsString } from "class-validator" import { CartService, PricingService, @@ -6,12 +5,13 @@ import { ProductVariantService, RegionService, } from "../../../../services" +import { IsInt, IsOptional, IsString } from "class-validator" -import { Type } from "class-transformer" +import { IsType } from "../../../../utils/validators/is-type" import { NumericalComparisonOperator } from "../../../../types/common" import { PriceSelectionParams } from "../../../../types/price-selection" +import { Type } from "class-transformer" import { validator } from "../../../../utils/validator" -import { IsType } from "../../../../utils/validators/is-type" /** * @oas [get] /store/variants @@ -154,11 +154,9 @@ export default async (req, res) => { const productVariantInventoryService: ProductVariantInventoryService = req.scope.resolve("productVariantInventoryService") const regionService: RegionService = req.scope.resolve("regionService") + const featureFlagRouter = req.scope.resolve("featureFlagRouter") - const rawVariants = await variantService.list( - filterableFields, - req.listConfig - ) + const variants = await variantService.list(filterableFields, req.listConfig) let regionId = validated.region_id let currencyCode = validated.currency_code @@ -173,19 +171,26 @@ export default async (req, res) => { currencyCode = region.currency_code } - const pricedVariants = await pricingService.setVariantPrices(rawVariants, { - cart_id: validated.cart_id, - region_id: regionId, - currency_code: currencyCode, - customer_id: customer_id, - include_discount_prices: true, - }) + const decoratePromises: Promise[] = [] - const variants = await productVariantInventoryService.setVariantAvailability( - pricedVariants, - sales_channel_id + decoratePromises.push( + (await pricingService.setVariantPrices(variants, { + cart_id: validated.cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: customer_id, + include_discount_prices: true, + })) as any ) + decoratePromises.push( + (await productVariantInventoryService.setVariantAvailability( + variants, + sales_channel_id + )) as any + ) + await Promise.all(decoratePromises) + res.json({ variants }) } diff --git a/packages/medusa/src/commands/migrate.js b/packages/medusa/src/commands/migrate.js index 3f85a13dd6..abc314fa53 100644 --- a/packages/medusa/src/commands/migrate.js +++ b/packages/medusa/src/commands/migrate.js @@ -1,14 +1,20 @@ import { asValue, createContainer } from "awilix" -import featureFlagLoader from "../loaders/feature-flags" -import Logger from "../loaders/logger" -import databaseLoader from "../loaders/database" -import configModuleLoader from "../loaders/config" import getMigrations, { getModuleSharedResources, revertIsolatedModulesMigration, runIsolatedModulesMigration, } from "./utils/get-migrations" +import { createMedusaContainer } from "@medusajs/utils" +import configModuleLoader from "../loaders/config" +import databaseLoader from "../loaders/database" +import featureFlagLoader from "../loaders/feature-flags" +import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" +import Logger from "../loaders/logger" +import { loadMedusaApp } from "../loaders/medusa-app" +import pgConnectionLoader from "../loaders/pg-connection" + const getDataSource = async (directory) => { const configModule = configModuleLoader(directory) const featureFlagRouter = featureFlagLoader(configModule) @@ -31,6 +37,25 @@ const getDataSource = async (directory) => { }) } +const runLinkMigrations = async (directory) => { + const configModule = configModuleLoader(directory) + const container = createMedusaContainer() + + await pgConnectionLoader({ configModule, container }) + + const { runMigrations } = await loadMedusaApp( + { configModule, container }, + { registerInContainer: false } + ) + + const options = { + database: { + clientUrl: configModule.projectConfig.database_url, + }, + } + await runMigrations(options) +} + const main = async function ({ directory }) { const args = process.argv @@ -40,19 +65,26 @@ 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) - Logger.info("Migrations completed.") + if ( + featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) || + featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key) + ) { + await runLinkMigrations(directory) + } process.exit() + + Logger.info("Migrations completed.") } else if (args[0] === "revert") { await dataSource.undoLastMigration({ transaction: "all" }) await dataSource.destroy() await revertIsolatedModulesMigration(configModule) Logger.info("Migrations reverted.") - process.exit() } else if (args[0] === "show") { const unapplied = await dataSource.showMigrations() Logger.info(unapplied) diff --git a/packages/medusa/src/commands/seed.ts b/packages/medusa/src/commands/seed.ts index f0ac604ceb..5cc4a5ea11 100644 --- a/packages/medusa/src/commands/seed.ts +++ b/packages/medusa/src/commands/seed.ts @@ -1,17 +1,4 @@ -import express from "express" -import fs from "fs" -import { sync as existsSync } from "fs-exists-cached" -import { getConfigFile } from "medusa-core-utils" -import { track } from "medusa-telemetry" -import path from "path" import { DataSource, DataSourceOptions } from "typeorm" - -import loaders from "../loaders" -import { handleConfigError } from "../loaders/config" -import Logger from "../loaders/logger" - -import featureFlagLoader from "../loaders/feature-flags" - import { ProductCategoryService, ProductService, @@ -23,12 +10,25 @@ import { StoreService, UserService, } from "../services" -import { ConfigModule } from "../types/global" -import { CreateProductInput } from "../types/product" -import { CreateProductCategoryInput } from "../types/product-category" import getMigrations, { getModuleSharedResources } from "./utils/get-migrations" + +import { ConfigModule } from "../types/global" +import { CreateProductCategoryInput } from "../types/product-category" +import { CreateProductInput } from "../types/product" +import { IPricingModuleService } from "@medusajs/types" +import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain" +import Logger from "../loaders/logger" import PublishableApiKeyService from "../services/publishable-api-key" import { SalesChannel } from "../models" +import { sync as existsSync } from "fs-exists-cached" +import express from "express" +import featureFlagLoader from "../loaders/feature-flags" +import fs from "fs" +import { getConfigFile } from "medusa-core-utils" +import { handleConfigError } from "../loaders/config" +import loaders from "../loaders" +import path from "path" +import { track } from "medusa-telemetry" type SeedOptions = { directory: string @@ -121,6 +121,9 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) { const shippingProfileService: ShippingProfileService = container.resolve( "shippingProfileService" ) + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) /* eslint-enable */ await manager.transaction(async (tx) => { @@ -131,6 +134,7 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) { categories = [], shipping_options, users, + rule_types = [], publishable_api_keys = [], } = JSON.parse(fs.readFileSync(resolvedPath, `utf-8`)) @@ -270,6 +274,14 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) { ]) } } + + if ( + featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key) + ) { + for (const ruleType of rule_types) { + await pricingModuleService.createRuleTypes(ruleType) + } + } }) track("CLI_SEED_COMPLETED") diff --git a/packages/medusa/src/index.js b/packages/medusa/src/index.js index b171cba753..9a46e91dfa 100644 --- a/packages/medusa/src/index.js +++ b/packages/medusa/src/index.js @@ -10,3 +10,5 @@ export * from "./types/routing" export * from "./types/global" export * from "./types/price-list" export * from "./utils" +export * from "./joiner-config" +export * from "./modules-config" diff --git a/packages/medusa/src/loaders/feature-flags/isolate-pricing-domain.ts b/packages/medusa/src/loaders/feature-flags/isolate-pricing-domain.ts index f522ecaa19..2a67acd3c8 100644 --- a/packages/medusa/src/loaders/feature-flags/isolate-pricing-domain.ts +++ b/packages/medusa/src/loaders/feature-flags/isolate-pricing-domain.ts @@ -1,10 +1,10 @@ import { FlagSettings } from "../../types/feature-flags" -const PricingIntegrationFeatureFlag: FlagSettings = { +const IsolatePricingDomainFeatureFlag: FlagSettings = { key: "isolate_pricing_domain", default_val: false, env_key: "MEDUSA_FF_ISOLATE_PRICING_DOMAIN", description: "[WIP] use price module integration for pricing", } -export default PricingIntegrationFeatureFlag +export default IsolatePricingDomainFeatureFlag diff --git a/packages/medusa/src/loaders/helpers/routing/index.ts b/packages/medusa/src/loaders/helpers/routing/index.ts index 3ccade4621..546fbf391c 100644 --- a/packages/medusa/src/loaders/helpers/routing/index.ts +++ b/packages/medusa/src/loaders/helpers/routing/index.ts @@ -535,7 +535,7 @@ export class RoutesLoader { } async load() { - performance.mark("file-base-routing-start" + this.rootDir) + performance && performance.mark("file-base-routing-start" + this.rootDir) let apiExists = true @@ -563,14 +563,16 @@ export class RoutesLoader { await this.registerRoutes() } - performance.mark("file-base-routing-end" + this.rootDir) - const timeSpent = performance - .measure( - "file-base-routing-measure" + this.rootDir, - "file-base-routing-start" + this.rootDir, - "file-base-routing-end" + this.rootDir - ) - ?.duration?.toFixed(2) + performance && performance.mark("file-base-routing-end" + this.rootDir) + const timeSpent = + performance && + performance + .measure( + "file-base-routing-measure" + this.rootDir, + "file-base-routing-start" + this.rootDir, + "file-base-routing-end" + this.rootDir + ) + ?.duration?.toFixed(2) log({ activityId: this.activityId, diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index f199b894d1..c5ed4e7184 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -1,43 +1,42 @@ import { - ExternalModuleDeclaration, - InternalModuleDeclaration, MedusaApp, - moduleLoader, ModulesDefinition, + moduleLoader, registerModules, } from "@medusajs/modules-sdk" +import { Express, NextFunction, Request, Response } from "express" + +import databaseLoader, { dataSource } from "./database" +import pluginsLoader, { registerPluginModels } from "./plugins" + +import { Connection } from "typeorm" import { ContainerRegistrationKeys } from "@medusajs/utils" import { asValue } from "awilix" -import { Express, NextFunction, Request, Response } from "express" import { createMedusaContainer } from "medusa-core-utils" import { track } from "medusa-telemetry" import { EOL } from "os" -import "reflect-metadata" import requestIp from "request-ip" -import { Connection } from "typeorm" -import { joinerConfig } from "../joiner-config" import modulesConfig from "../modules-config" import { MedusaContainer } from "../types/global" -import { isObject, remoteQueryFetchData } from "../utils" import apiLoader from "./api" -import loadConfig from "./config" -import databaseLoader, { dataSource } from "./database" import defaultsLoader from "./defaults" import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" +import IsolatePricingDomainFeatureFlag from "./feature-flags/isolate-pricing-domain" import IsolateProductDomainFeatureFlag from "./feature-flags/isolate-product-domain" import Logger from "./logger" +import { joinerConfig } from "../joiner-config" +import loadConfig from "./config" import modelsLoader from "./models" import passportLoader from "./passport" import pgConnectionLoader from "./pg-connection" -import pluginsLoader, { registerPluginModels } from "./plugins" import redisLoader from "./redis" import repositoriesLoader from "./repositories" import searchIndexLoader from "./search-index" import servicesLoader from "./services" import strategiesLoader from "./strategies" import subscribersLoader from "./subscribers" -import { ConfigModule } from "@medusajs/types" +import loadMedusaApp from "./medusa-app" type Options = { directory: string @@ -45,34 +44,6 @@ type Options = { isTest: boolean } -/** - * Merge the modules config from the medusa-config file with the modules config from medusa package - * @param modules - * @param medusaInternalModulesConfig - */ -function mergeModulesConfig( - modules: ConfigModule["modules"], - medusaInternalModulesConfig -) { - for (const [moduleName, moduleConfig] of Object.entries(modules as any)) { - const moduleDefinition = ModulesDefinition[moduleName] - - if (moduleDefinition?.isLegacy) { - continue - } - - const isModuleEnabled = moduleConfig === true || isObject(moduleConfig) - - if (!isModuleEnabled) { - delete medusaInternalModulesConfig[moduleName] - } else { - medusaInternalModulesConfig[moduleName] = moduleConfig as Partial< - InternalModuleDeclaration | ExternalModuleDeclaration - > - } - } -} - export default async ({ directory: rootDirectory, expressApp, @@ -138,11 +109,7 @@ export default async ({ track("MODULES_INIT_STARTED") await moduleLoader({ container, - moduleResolutions: registerModules(configModule?.modules, { - loadLegacyOnly: featureFlagRouter.isFeatureEnabled( - IsolateProductDomainFeatureFlag.key - ), - }), + moduleResolutions: registerModules(configModule?.modules), logger: Logger, }) const modAct = Logger.success(modulesActivity, "Modules initialized") || {} @@ -167,6 +134,15 @@ export default async ({ [ContainerRegistrationKeys.MANAGER]: asValue(dataSource.manager), }) + container.register("remoteQuery", asValue(null)) // ensure remoteQuery is always registered + // Only load non legacy modules, the legacy modules (non migrated yet) are retrieved by the registerModule above + if ( + featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) || + featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key) + ) { + await loadMedusaApp({ configModule, container }) + } + const servicesActivity = Logger.activity(`Initializing services${EOL}`) track("SERVICES_INIT_STARTED") servicesLoader({ container, configModule, isTest }) @@ -226,32 +202,5 @@ export default async ({ Logger.success(searchActivity, "Indexing event emitted") || {} track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) - // Only load non legacy modules, the legacy modules (non migrated yet) are retrieved by the registerModule above - if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) { - mergeModulesConfig(configModule.modules ?? {}, modulesConfig) - - const { query, modules } = await MedusaApp({ - modulesConfig, - servicesConfig: joinerConfig, - remoteFetchData: remoteQueryFetchData(container), - sharedContainer: container, - injectedDependencies: { - [ContainerRegistrationKeys.PG_CONNECTION]: container.resolve( - ContainerRegistrationKeys.PG_CONNECTION - ), - }, - }) - - // Medusa app load all non legacy modules, so we need to register them in the container since they are into their own container - // We might decide to do it elsewhere but for now I think it is fine - for (const [serviceKey, moduleService] of Object.entries(modules)) { - container.register( - ModulesDefinition[serviceKey].registrationName, - asValue(moduleService) - ) - } - container.register("remoteQuery", asValue(query)) - } - return { container, dbConnection, app: expressApp } } diff --git a/packages/medusa/src/loaders/medusa-app.ts b/packages/medusa/src/loaders/medusa-app.ts new file mode 100644 index 0000000000..27c5c95176 --- /dev/null +++ b/packages/medusa/src/loaders/medusa-app.ts @@ -0,0 +1,67 @@ +import { CommonTypes, MedusaContainer } from "@medusajs/types" +import { + MedusaApp, + MedusaAppOutput, + ModulesDefinition, +} from "@medusajs/modules-sdk" + +import { ContainerRegistrationKeys } from "@medusajs/utils" +import { asValue } from "awilix" +import { joinerConfig } from "../joiner-config" +import { mergeModulesConfig } from "../utils/merge-modules-config" +import modulesConfig from "../modules-config" +import { remoteQueryFetchData } from ".." + +export const loadMedusaApp = async ( + { + configModule, + container, + }: { configModule: CommonTypes.ConfigModule; container: MedusaContainer }, + config = { registerInContainer: true } +): Promise => { + mergeModulesConfig(configModule.modules ?? {}, modulesConfig) + + const injectedDependencies = { + [ContainerRegistrationKeys.PG_CONNECTION]: container.resolve( + ContainerRegistrationKeys.PG_CONNECTION + ), + } + + const sharedResourcesConfig = { + database: { + clientUrl: configModule.projectConfig.database_url, + driverOptions: configModule.projectConfig.database_extra, + }, + } + + const medusaApp = await MedusaApp({ + modulesConfig, + servicesConfig: joinerConfig, + remoteFetchData: remoteQueryFetchData(container), + sharedContainer: container, + sharedResourcesConfig, + injectedDependencies, + }) + + if (!config.registerInContainer) { + return medusaApp + } + + container.register("remoteLink", asValue(medusaApp.link)) + + const { query, modules } = medusaApp + + // Medusa app load all non legacy modules, so we need to register them in the container since they are into their own container + // We might decide to do it elsewhere but for now I think it is fine + for (const [serviceKey, moduleService] of Object.entries(modules)) { + container.register( + ModulesDefinition[serviceKey].registrationName, + asValue(moduleService) + ) + } + container.register("remoteQuery", asValue(query)) + + return medusaApp +} + +export default loadMedusaApp diff --git a/packages/medusa/src/loaders/pg-connection.ts b/packages/medusa/src/loaders/pg-connection.ts index b522b2155a..cfd6ac3068 100644 --- a/packages/medusa/src/loaders/pg-connection.ts +++ b/packages/medusa/src/loaders/pg-connection.ts @@ -1,13 +1,13 @@ -import { asValue, AwilixContainer } from "awilix" -import { ConfigModule } from "../types/global" import { ContainerRegistrationKeys, ModulesSdkUtils } from "@medusajs/utils" +import { AwilixContainer, asValue } from "awilix" +import { ConfigModule } from "../types/global" type Options = { configModule: ConfigModule container: AwilixContainer } -export default async ({ container, configModule }: Options): Promise => { +export default async ({ container, configModule }: Options): Promise => { if (container.hasRegistration(ContainerRegistrationKeys.PG_CONNECTION)) { return } @@ -36,4 +36,6 @@ export default async ({ container, configModule }: Options): Promise => { ContainerRegistrationKeys.PG_CONNECTION, asValue(pgConnection) ) + + return pgConnection } diff --git a/packages/medusa/src/scripts/create-default-rule-types.ts b/packages/medusa/src/scripts/create-default-rule-types.ts new file mode 100644 index 0000000000..66e26996b6 --- /dev/null +++ b/packages/medusa/src/scripts/create-default-rule-types.ts @@ -0,0 +1,44 @@ +import { AwilixContainer } from "awilix" +import { IPricingModuleService } from "@medusajs/types" +import dotenv from "dotenv" +import express from "express" +import loaders from "../loaders" + +dotenv.config() + +export const createDefaultRuleTypes = async (container: AwilixContainer) => { + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + const existing = await pricingModuleService.listRuleTypes( + { rule_attribute: ["region_id"] }, + { take: 1 } + ) + + if (existing.length) { + return + } + + await pricingModuleService.createRuleTypes([ + { name: "region_id", rule_attribute: "region_id" }, + ]) +} + +const migrate = async function ({ directory }) { + const app = express() + const { container } = await loaders({ + directory, + expressApp: app, + isTest: false, + }) + + return await createDefaultRuleTypes(container) +} + +migrate({ directory: process.cwd() }) + .then(() => { + console.log("Created default rule types") + }) + .catch(() => { + console.log("Failed to create rule types") + }) diff --git a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts b/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts new file mode 100644 index 0000000000..1cdb442505 --- /dev/null +++ b/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts @@ -0,0 +1,142 @@ +import { FlagRouter, MedusaError } from "@medusajs/utils" +import { IPricingModuleService, MedusaContainer } from "@medusajs/types" + +import { AwilixContainer } from "awilix" +import { EntityManager } from "typeorm" +import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain" +import { Modules } from "@medusajs/modules-sdk" +import { ProductVariant } from "../models" +import { ProductVariantService } from "../services" +import { createDefaultRuleTypes } from "./create-default-rule-types" +import dotenv from "dotenv" +import express from "express" +import loadMedusaApp from "../loaders/medusa-app" +import loaders from "../loaders" + +dotenv.config() + +const BATCH_SIZE = 100 + +const migrateProductVariant = async ( + variant: ProductVariant, + { + container, + }: { container: MedusaContainer; transactionManager: EntityManager } +) => { + const pricingService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + + const configModule = await container.resolve("configModule") + const { link } = await loadMedusaApp( + { configModule, container }, + { registerInContainer: false } + ) + + if (!link) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't migrate money_amounts: Pricing module is not configured correctly" + ) + } + + const priceSet = await pricingService.create({ + rules: [{ rule_attribute: "region_id" }], + prices: variant.prices.map((price) => ({ + rules: { + region_id: price.region_id, + }, + currency_code: price.currency_code, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + amount: price.amount, + })), + }) + + await link.create({ + productService: { + variant_id: variant.id, + }, + pricingService: { + price_set_id: priceSet.id, + }, + }) +} + +const processBatch = async ( + variants: ProductVariant[], + container: MedusaContainer +) => { + const manager = container.resolve("manager") + return await manager.transaction(async (transactionManager) => { + await Promise.all( + variants.map(async (variant) => { + await migrateProductVariant(variant, { + container, + transactionManager, + }) + }) + ) + }) +} + +const migrate = async function ({ directory }) { + const app = express() + const { container } = await loaders({ + directory, + expressApp: app, + isTest: false, + }) + + const variantService: ProductVariantService = await container.resolve( + "productVariantService" + ) + const featureFlagRouter: FlagRouter = await container.resolve( + "featureFlagRouter" + ) + + if ( + !featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key) && + !featureFlagRouter.isFeatureEnabled(Modules.PRICING) + ) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Pricing module not enabled" + ) + } + + await createDefaultRuleTypes(container) + + const [variants, totalCount] = await variantService.listAndCount( + {}, + { take: BATCH_SIZE, order: { id: "ASC" }, relations: ["prices"] } + ) + + await processBatch(variants, container) + + let processedCount = variants.length + + console.log(`Processed ${processedCount} of ${totalCount}`) + + while (processedCount < totalCount) { + const nextBatch = await variantService.list( + {}, + { + skip: processedCount, + take: BATCH_SIZE, + order: { id: "ASC" }, + relations: ["prices"], + } + ) + + await processBatch(nextBatch, container) + + processedCount += nextBatch.length + console.log(`Processed ${processedCount} of ${totalCount}`) + } + + console.log("Done") + process.exit(0) +} + +migrate({ directory: process.cwd() }) diff --git a/packages/medusa/src/services/__mocks__/pricing.js b/packages/medusa/src/services/__mocks__/pricing.js index 1783c79216..05608a4340 100644 --- a/packages/medusa/src/services/__mocks__/pricing.js +++ b/packages/medusa/src/services/__mocks__/pricing.js @@ -5,9 +5,15 @@ export const PricingServiceMock = { setProductPrices: jest.fn().mockImplementation((prod) => { return Promise.resolve(prod) }), + setAdminProductPricing: jest.fn().mockImplementation((prod) => { + return Promise.resolve(prod) + }), setVariantPrices: jest.fn().mockImplementation(([variant]) => { return Promise.resolve([variant]) }), + setAdminVariantPricing: jest.fn().mockImplementation(([variant]) => { + return Promise.resolve([variant]) + }), setShippingOptionPrices: jest.fn().mockImplementation((opts) => { return Promise.resolve(opts) }), diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index ba0ef88e0a..bc657885c1 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -1,28 +1,29 @@ -import { FlagRouter } from "@medusajs/utils" -import { asClass, asValue, createContainer } from "awilix" -import _ from "lodash" -import { MedusaError } from "medusa-core-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IsNull, Not } from "typeorm" -import { PaymentSessionStatus } from "../../models" -import TaxCalculationStrategy from "../../strategies/tax-calculation" -import { cacheServiceMock } from "../__mocks__/cache" +import { NewTotalsService, PricingService, TaxProviderService } from "../index" +import { asClass, asValue, createContainer } from "awilix" + +import CartService from "../cart" import { CustomerServiceMock } from "../__mocks__/customer" import { EventBusServiceMock } from "../__mocks__/event-bus" -import { LineItemServiceMock } from "../__mocks__/line-item" +import { FlagRouter } from "@medusajs/utils" import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment" -import { newTotalsServiceMock } from "../__mocks__/new-totals" +import { LineItemServiceMock } from "../__mocks__/line-item" +import { MedusaError } from "medusa-core-utils" import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" +import { PaymentSessionStatus } from "../../models" import { ProductServiceMock } from "../__mocks__/product" -import { ProductVariantServiceMock } from "../__mocks__/product-variant" import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory" +import { ProductVariantServiceMock } from "../__mocks__/product-variant" import { RegionServiceMock } from "../__mocks__/region" import { ShippingOptionServiceMock } from "../__mocks__/shipping-option" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" -import { taxProviderServiceMock } from "../__mocks__/tax-provider" -import CartService from "../cart" -import { NewTotalsService, PricingService, TaxProviderService } from "../index" import SystemTaxService from "../system-tax" +import TaxCalculationStrategy from "../../strategies/tax-calculation" +import _ from "lodash" +import { cacheServiceMock } from "../__mocks__/cache" +import { newTotalsServiceMock } from "../__mocks__/new-totals" +import { taxProviderServiceMock } from "../__mocks__/tax-provider" const eventBusService = { emit: jest.fn(), @@ -2657,6 +2658,8 @@ describe("CartService", () => { .register("taxProviderService", asClass(TaxProviderService)) .register("newTotalsService", asClass(NewTotalsService)) .register("cartService", asClass(CartService)) + .register("remoteQuery", asValue(null)) + .register("pricingModuleService", asValue(undefined)) .register("pricingService", asClass(PricingService)) const cartService = container.resolve("cartService") diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index 4bbf411b74..891db66741 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -1,14 +1,22 @@ -import { FlagRouter } from "@medusajs/utils" -import { MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" -import { ProductVariantService, RegionService, TaxProviderService } from "." -import { TransactionBaseService } from "../interfaces" +import { + CalculatedPriceSetDTO, + IPricingModuleService, + PriceSetMoneyAmountDTO, + RemoteJoinerQuery, + RemoteQueryFunction, +} from "@medusajs/types" +import { FlagRouter, removeNullish } from "@medusajs/utils" import { IPriceSelectionStrategy, PriceSelectionContext, } from "../interfaces/price-selection-strategy" -import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" -import { Product, ProductVariant, Region, ShippingOption } from "../models" +import { + MoneyAmount, + Product, + ProductVariant, + Region, + ShippingOption, +} from "../models" import { PricedProduct, PricedShippingOption, @@ -17,7 +25,15 @@ import { ProductVariantPricing, TaxedPricing, } from "../types/pricing" +import { ProductVariantService, RegionService, TaxProviderService } from "." + +import { EntityManager } from "typeorm" +import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" +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 = { @@ -27,6 +43,8 @@ type InjectedDependencies = { regionService: RegionService priceSelectionStrategy: IPriceSelectionStrategy featureFlagRouter: FlagRouter + remoteQuery: RemoteQueryFunction + pricingModuleService: IPricingModuleService } /** @@ -38,6 +56,8 @@ class PricingService extends TransactionBaseService { protected readonly priceSelectionStrategy: IPriceSelectionStrategy protected readonly productVariantService: ProductVariantService protected readonly featureFlagRouter: FlagRouter + protected readonly pricingModuleService: IPricingModuleService + protected readonly remoteQuery: RemoteQueryFunction constructor({ productVariantService, @@ -45,6 +65,8 @@ class PricingService extends TransactionBaseService { regionService, priceSelectionStrategy, featureFlagRouter, + remoteQuery, + pricingModuleService, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -54,6 +76,8 @@ class PricingService extends TransactionBaseService { this.priceSelectionStrategy = priceSelectionStrategy this.productVariantService = productVariantService this.featureFlagRouter = featureFlagRouter + this.pricingModuleService = pricingModuleService + this.remoteQuery = remoteQuery } /** @@ -160,6 +184,90 @@ class PricingService extends TransactionBaseService { return taxedPricing } + private async getProductVariantPricingModulePricing_( + variantPriceData: { + variantId: string + quantity?: number + }[], + context: PricingContext + ) { + const variables = { + variant_id: variantPriceData.map((pricedata) => pricedata.variantId), + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await this.remoteQuery(query) + + const variantIdToPriceSetIdMap: Map = new Map( + variantPriceSets.map((variantPriceSet) => [ + variantPriceSet.variant_id, + variantPriceSet.price_set_id, + ]) + ) + + const priceSetIds: string[] = variantPriceSets.map( + (variantPriceSet) => variantPriceSet.price_set_id + ) + + const queryContext: PriceSelectionContext = removeNullish( + context.price_selection + ) + + let priceSets: CalculatedPriceSetDTO[] = [] + + if (queryContext.currency_code) { + priceSets = (await this.pricingModuleService.calculatePrices( + { id: priceSetIds }, + { + context: queryContext as any, + } + )) as unknown as CalculatedPriceSetDTO[] + } + + const priceSetMap = new Map( + priceSets.map((priceSet) => [priceSet.id, priceSet]) + ) + + const pricingResultMap = new Map() + + variantPriceData.forEach(({ variantId }) => { + const priceSetId = variantIdToPriceSetIdMap.get(variantId) + + const pricingResult: ProductVariantPricing = { + prices: [] as MoneyAmount[], + original_price: null, + calculated_price: null, + calculated_price_type: null, + original_price_includes_tax: null, + calculated_price_includes_tax: null, + original_price_incl_tax: null, + calculated_price_incl_tax: null, + original_tax: null, + calculated_tax: null, + tax_rates: null, + } + + if (priceSetId) { + const prices = priceSetMap.get(priceSetId) + + if (prices) { + pricingResult.prices = [prices] as MoneyAmount[] + pricingResult.original_price = prices.amount + pricingResult.calculated_price = prices.amount + } + } + pricingResultMap.set(variantId, pricingResult) + }) + + return pricingResultMap + } + private async getProductVariantPricing_( data: { variantId: string @@ -167,6 +275,17 @@ class PricingService extends TransactionBaseService { }[], context: PricingContext ): Promise> { + if ( + this.featureFlagRouter.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ) && + this.featureFlagRouter.isFeatureEnabled( + IsolatePricingDomainFeatureFlag.key + ) + ) { + return await this.getProductVariantPricingModulePricing_(data, context) + } + const variantsPricing = await this.priceSelectionStrategy .withTransaction(this.activeManager_) .calculateVariantPrice(data, context.price_selection) @@ -509,6 +628,7 @@ class PricingService extends TransactionBaseService { product.variants.map((productVariant): PricedVariant => { const variantPricing = productsVariantsPricingMap.get(product.id)! const pricing = variantPricing[productVariant.id] + Object.assign(productVariant, pricing) return productVariant as unknown as PricedVariant }) @@ -517,6 +637,167 @@ class PricingService extends TransactionBaseService { }) } + private async getPricingModuleVariantMoneyAmounts( + variantIds: string[] + ): Promise> { + const variables = { + variant_id: variantIds, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await this.remoteQuery(query) + + const priceSetIdToVariantIdMap: Map = new Map( + variantPriceSets.map((variantPriceSet) => [ + variantPriceSet.price_set_id, + variantPriceSet.variant_id, + ]) + ) + + const priceSetIds: string[] = variantPriceSets.map( + (variantPriceSet) => variantPriceSet.price_set_id + ) + + const priceSetMoneyAmounts: PriceSetMoneyAmountDTO[] = + await this.pricingModuleService.listPriceSetMoneyAmounts( + { + price_set_id: priceSetIds, + }, + { + relations: [ + "money_amount", + "price_set", + "price_rules", + "price_rules.rule_type", + ], + } + ) + + const variantIdMoneyAmountMap = priceSetMoneyAmounts.reduce( + (map, priceSetMoneyAmount) => { + const variantId = priceSetIdToVariantIdMap.get( + priceSetMoneyAmount.price_set!.id + ) + if (!variantId) { + return map + } + + const regionId = priceSetMoneyAmount.price_rules!.find( + (pr) => pr.rule_type.rule_attribute === "region_id" + )?.value + + const moneyAmount = { + ...priceSetMoneyAmount.money_amount, + region_id: null as null | string, + } + + if (regionId) { + moneyAmount.region_id = regionId + } + + if (map.has(variantId)) { + map.get(variantId).push(moneyAmount) + } else { + map.set(variantId, [moneyAmount]) + } + return map + }, + new Map() + ) + + return variantIdMoneyAmountMap + } + + async setAdminVariantPricing( + variants: ProductVariant[], + context: PriceSelectionContext = {} + ): Promise { + if ( + !this.featureFlagRouter.isFeatureEnabled( + IsolatePricingDomainFeatureFlag.key + ) + ) { + return await this.setVariantPrices(variants, context) + } + + const variantIds = variants.map((variant) => variant.id) + + const variantIdMoneyAmountMap = + await this.getPricingModuleVariantMoneyAmounts(variantIds) + + return variants.map((variant) => { + const pricing: ProductVariantPricing = { + prices: variantIdMoneyAmountMap.get(variant.id) ?? [], + original_price: null, + calculated_price: null, + calculated_price_type: null, + original_price_includes_tax: null, + calculated_price_includes_tax: null, + original_price_incl_tax: null, + calculated_price_incl_tax: null, + original_tax: null, + calculated_tax: null, + tax_rates: null, + } + + Object.assign(variant, pricing) + return variant as unknown as PricedVariant + }) + } + + async setAdminProductPricing( + products: Product[] + ): Promise<(Product | PricedProduct)[]> { + if ( + !this.featureFlagRouter.isFeatureEnabled( + IsolatePricingDomainFeatureFlag.key + ) + ) { + return await this.setProductPrices(products) + } + + const variantIds = products + .map((product) => product.variants.map((variant) => variant.id).flat()) + .flat() + + const variantIdMoneyAmountMap = + await this.getPricingModuleVariantMoneyAmounts(variantIds) + + return products.map((product) => { + if (!product?.variants?.length) { + return product + } + + product.variants.map((productVariant): PricedVariant => { + const pricing: ProductVariantPricing = { + prices: variantIdMoneyAmountMap.get(productVariant.id) ?? [], + original_price: null, + calculated_price: null, + calculated_price_type: null, + original_price_includes_tax: null, + calculated_price_includes_tax: null, + original_price_incl_tax: null, + calculated_price_incl_tax: null, + original_tax: null, + calculated_tax: null, + tax_rates: null, + } + + Object.assign(productVariant, pricing) + + return productVariant as unknown as PricedVariant + }) + + return product + }) + } + /** * Gets the prices for a shipping option. * @param shippingOption - the shipping option to get prices for diff --git a/packages/medusa/src/types/pricing.ts b/packages/medusa/src/types/pricing.ts index 183bb8ae04..e4d13941cd 100644 --- a/packages/medusa/src/types/pricing.ts +++ b/packages/medusa/src/types/pricing.ts @@ -124,3 +124,14 @@ export type PricedVariant = Partial & ProductVariantPricing export type PricedProduct = Omit, "variants"> & { variants: PricedVariant[] } + +export type VariantPriceSetRes = { + id: string + title: string + price: PriceModulePrice | PriceModulePrice[] +} + +type PriceModulePrice = { + variant_id: string + price_set_id: string +} diff --git a/packages/medusa/src/utils/merge-modules-config.ts b/packages/medusa/src/utils/merge-modules-config.ts new file mode 100644 index 0000000000..cd238cc400 --- /dev/null +++ b/packages/medusa/src/utils/merge-modules-config.ts @@ -0,0 +1,36 @@ +import { + ConfigModule, + ExternalModuleDeclaration, + InternalModuleDeclaration, +} from "@medusajs/types" + +import { ModulesDefinition } from "@medusajs/modules-sdk" +import { isObject } from "./is-object" + +/** + * Merge the modules config from the medusa-config file with the modules config from medusa package + * @param modules + * @param medusaInternalModulesConfig + */ +export function mergeModulesConfig( + modules: ConfigModule["modules"], + medusaInternalModulesConfig +) { + for (const [moduleName, moduleConfig] of Object.entries(modules as any)) { + const moduleDefinition = ModulesDefinition[moduleName] + + if (moduleDefinition?.isLegacy) { + continue + } + + const isModuleEnabled = moduleConfig === true || isObject(moduleConfig) + + if (!isModuleEnabled) { + delete medusaInternalModulesConfig[moduleName] + } else { + medusaInternalModulesConfig[moduleName] = moduleConfig as Partial< + InternalModuleDeclaration | ExternalModuleDeclaration + > + } + } +} diff --git a/packages/modules-sdk/src/loaders/utils/load-internal.ts b/packages/modules-sdk/src/loaders/utils/load-internal.ts index 810d3698cf..d7ff0dd009 100644 --- a/packages/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/modules-sdk/src/loaders/utils/load-internal.ts @@ -139,7 +139,8 @@ export async function loadModuleMigrations( ): Promise<[Function | undefined, Function | undefined]> { let loadedModule: ModuleExports try { - loadedModule = (await import(resolution.resolutionPath as string)).default + loadedModule = await import(resolution.resolutionPath as string) + return [loadedModule.runMigrations, loadedModule.revertMigration] } catch { return [undefined, undefined] diff --git a/packages/modules-sdk/src/medusa-app.ts b/packages/modules-sdk/src/medusa-app.ts index 41ba0a8aae..6521e06e17 100644 --- a/packages/modules-sdk/src/medusa-app.ts +++ b/packages/modules-sdk/src/medusa-app.ts @@ -1,6 +1,3 @@ -import { mergeTypeDefs } from "@graphql-tools/merge" -import { makeExecutableSchema } from "@graphql-tools/schema" -import { RemoteFetchDataCallback } from "@medusajs/orchestration" import { ExternalModuleDeclaration, InternalModuleDeclaration, @@ -26,15 +23,18 @@ import { Modules, } from "./definitions" import { MedusaModule } from "./medusa-module" +import { RemoteFetchDataCallback } from "@medusajs/orchestration" import { RemoteLink } from "./remote-link" import { RemoteQuery } from "./remote-query" import { cleanGraphQLSchema } from "./utils" import { asValue } from "awilix" +import { makeExecutableSchema } from "@graphql-tools/schema" +import { mergeTypeDefs } from "@graphql-tools/merge" const LinkModulePackage = "@medusajs/link-modules" export type RunMigrationFn = ( - options: Omit, "container">, + options?: ModuleServiceInitializeOptions, injectedDependencies?: Record ) => Promise @@ -162,6 +162,18 @@ function registerCustomJoinerConfigs(servicesConfig: ModuleJoinerConfig[]) { } } +export type MedusaAppOutput = { + modules: Record + link: RemoteLink | undefined + query: ( + query: string | RemoteJoinerQuery | object, + variables?: Record + ) => Promise + entitiesMap?: Record + notFound?: Record> + runMigrations: RunMigrationFn +} + export async function MedusaApp( { sharedContainer, @@ -276,19 +288,24 @@ export async function MedusaApp( return await remoteQuery.query(query, variables) } - const runMigrations: RunMigrationFn = async (): Promise => { + const runMigrations: RunMigrationFn = async ( + linkModuleOptions + ): Promise => { for (const moduleName of Object.keys(allModules)) { - const loadedModule = allModules[moduleName] + const moduleResolution = MedusaModule.getModuleResolutions(moduleName) await MedusaModule.migrateUp( - loadedModule.definition.key, - loadedModule.resolutionPath, - loadedModule.options + moduleResolution.definition.key, + moduleResolution.resolutionPath as string, + moduleResolution.options ) } linkModuleMigration && - (await linkModuleMigration(linkResolution.options, injectedDependencies)) + (await linkModuleMigration({ + options: linkModuleOptions, + injectedDependencies, + })) } return { diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 845232e95a..6b2a803341 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -71,6 +71,7 @@ export class MedusaModule { private static modules_: Map = new Map() private static loading_: Map> = new Map() private static joinerConfig_: Map = new Map() + private static moduleResolutions_: Map = new Map() public static getLoadedModules( aliases?: Map @@ -88,6 +89,7 @@ export class MedusaModule { MedusaModule.instances_.clear() MedusaModule.modules_.clear() MedusaModule.joinerConfig_.clear() + MedusaModule.moduleResolutions_.clear() } public static isInstalled(moduleKey: string, alias?: string): boolean { @@ -109,11 +111,29 @@ export class MedusaModule { return [...MedusaModule.joinerConfig_.values()] } + public static getModuleResolutions(moduleKey: string): ModuleResolution { + return MedusaModule.moduleResolutions_.get(moduleKey)! + } + + public static getAllModuleResolutions(): ModuleResolution[] { + return [...MedusaModule.moduleResolutions_.values()] + } + + public static setModuleResolution( + moduleKey: string, + resolution: ModuleResolution + ): ModuleResolution { + MedusaModule.moduleResolutions_.set(moduleKey, resolution) + + return resolution + } + public static setJoinerConfig( moduleKey: string, config: ModuleJoinerConfig ): ModuleJoinerConfig { MedusaModule.joinerConfig_.set(moduleKey, config) + return config } @@ -261,6 +281,8 @@ export class MedusaModule { MedusaModule.setJoinerConfig(keyName, joinerConfig) } + MedusaModule.setModuleResolution(keyName, resolution) + MedusaModule.registerModule(keyName, { key: keyName, hash: hashKey, @@ -379,6 +401,7 @@ export class MedusaModule { } } + MedusaModule.setModuleResolution(keyName, resolution) MedusaModule.registerModule(keyName, { key: keyName, hash: hashKey, diff --git a/packages/modules-sdk/src/remote-query.ts b/packages/modules-sdk/src/remote-query.ts index 6db10af712..afe8228885 100644 --- a/packages/modules-sdk/src/remote-query.ts +++ b/packages/modules-sdk/src/remote-query.ts @@ -1,8 +1,3 @@ -import { - RemoteFetchDataCallback, - RemoteJoiner, - toRemoteJoinerQuery, -} from "@medusajs/orchestration" import { JoinerRelationship, JoinerServiceConfig, @@ -11,7 +6,13 @@ import { RemoteExpandProperty, RemoteJoinerQuery, } from "@medusajs/types" +import { + RemoteFetchDataCallback, + RemoteJoiner, + toRemoteJoinerQuery, +} from "@medusajs/orchestration" import { isString, toPascalCase } from "@medusajs/utils" + import { MedusaModule } from "./medusa-module" export class RemoteQuery { @@ -28,14 +29,14 @@ export class RemoteQuery { customRemoteFetchData?: RemoteFetchDataCallback servicesConfig?: ModuleJoinerConfig[] }) { + const servicesConfig_ = [...servicesConfig] + if (!modulesLoaded?.length) { modulesLoaded = MedusaModule.getLoadedModules().map( (mod) => Object.values(mod)[0] ) } - const servicesConfig_ = [...servicesConfig] - for (const mod of modulesLoaded) { if (!mod.__definition.isQueryable) { continue @@ -54,6 +55,7 @@ export class RemoteQuery { } this.customRemoteFetchData = customRemoteFetchData + this.remoteJoiner = new RemoteJoiner( servicesConfig_ as JoinerServiceConfig[], this.remoteFetchData.bind(this) diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 4528a67fa2..fc3dcc3641 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -40,6 +40,7 @@ import { MedusaContext, MedusaError, groupBy, + removeNullish, shouldForceTransaction, } from "@medusajs/utils" @@ -312,7 +313,9 @@ export default class PricingModuleService< sharedContext ) - const numberOfRules = ma.rules ? Object.entries(ma.rules).length : 0 + const cleanRules = ma.rules ? removeNullish(ma.rules) : {} + + const numberOfRules = Object.entries(cleanRules).length const [priceSetMoneyAmount] = await this.priceSetMoneyAmountService_.create( @@ -328,7 +331,7 @@ export default class PricingModuleService< ) if (numberOfRules) { - const priceSetRulesCreate = Object.entries(ma.rules).map( + const priceSetRulesCreate = Object.entries(cleanRules).map( ([k, v]) => ({ price_set_money_amount: priceSetMoneyAmount, rule_type: ruleTypeMap.get(k), @@ -1017,6 +1020,48 @@ export default class PricingModuleService< ] } + @InjectManager("baseRepository_") + async listPriceSetMoneyAmounts( + filters: PricingTypes.FilterablePriceSetMoneyAmountProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const records = await this.priceSetMoneyAmountService_.list( + filters, + config, + sharedContext + ) + + return this.baseRepository_.serialize< + PricingTypes.PriceSetMoneyAmountRulesDTO[] + >(records, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async listAndCountPriceSetMoneyAmounts( + filters: PricingTypes.FilterablePriceSetMoneyAmountProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PricingTypes.PriceSetMoneyAmountDTO[], number]> { + const [records, count] = + await this.priceSetMoneyAmountService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize< + PricingTypes.PriceSetMoneyAmountRulesDTO[] + >(records, { + populate: true, + }), + count, + ] + } + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") async createPriceSetMoneyAmountRules( data: PricingTypes.CreatePriceSetMoneyAmountRulesDTO[], diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index dedaea969c..43ff5622a8 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -1,8 +1,8 @@ -import { JoinerRelationship, JoinerServiceConfig } from "../joiner" +import { JoinerRelationship, JoinerServiceConfig, RemoteJoinerQuery } from "../joiner" +import { Logger } from "../logger" import { MedusaContainer } from "../common" import { RepositoryService } from "../dal" -import { Logger } from "../logger" export type Constructor = new (...args: any[]) => T export * from "../common/medusa-container" @@ -260,3 +260,8 @@ export type ModuleServiceInitializeCustomDataLayerOptions = { [key: string]: Constructor } } + +export type RemoteQueryFunction = ( + query: string | RemoteJoinerQuery | object, + variables?: Record +) => Promise | null \ No newline at end of file diff --git a/packages/types/src/pricing/common/money-amount.ts b/packages/types/src/pricing/common/money-amount.ts index 96bd1ba94b..420824e0b4 100644 --- a/packages/types/src/pricing/common/money-amount.ts +++ b/packages/types/src/pricing/common/money-amount.ts @@ -39,8 +39,8 @@ export interface CreateMoneyAmountDTO { currency_code: string currency?: CreateCurrencyDTO amount?: number - min_quantity?: number - max_quantity?: number + min_quantity?: number | null + max_quantity?: number | null } /** diff --git a/packages/types/src/pricing/common/price-set-money-amount.ts b/packages/types/src/pricing/common/price-set-money-amount.ts index b5a39e05d4..2dc983e78d 100644 --- a/packages/types/src/pricing/common/price-set-money-amount.ts +++ b/packages/types/src/pricing/common/price-set-money-amount.ts @@ -1,4 +1,6 @@ +import { BaseFilterable } from "../../dal" import { MoneyAmountDTO } from "./money-amount" +import { PriceRuleDTO } from "./price-rule" import { PriceSetDTO } from "./price-set" /** @@ -15,6 +17,8 @@ export interface PriceSetMoneyAmountDTO { id: string title?: string price_set?: PriceSetDTO + price_set_id?: string + price_rules?: PriceRuleDTO[] money_amount?: MoneyAmountDTO } @@ -30,3 +34,9 @@ export interface CreatePriceSetMoneyAmountDTO { price_set?: PriceSetDTO | string money_amount?: MoneyAmountDTO | string } + +export interface FilterablePriceSetMoneyAmountProps + extends BaseFilterable { + id?: string[] + price_set_id?: string[] +} diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index fbbd582d02..12bc2c1970 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -12,12 +12,14 @@ import { FilterableCurrencyProps, FilterableMoneyAmountProps, FilterablePriceRuleProps, + FilterablePriceSetMoneyAmountProps, FilterablePriceSetMoneyAmountRulesProps, FilterablePriceSetProps, FilterableRuleTypeProps, MoneyAmountDTO, PriceRuleDTO, PriceSetDTO, + PriceSetMoneyAmountDTO, PriceSetMoneyAmountRulesDTO, PricingContext, PricingFilters, @@ -31,9 +33,9 @@ import { UpdateRuleTypeDTO, } from "./common" +import { Context } from "../shared-context" import { FindConfig } from "../common" import { ModuleJoinerConfig } from "../modules-sdk" -import { Context } from "../shared-context" export interface IPricingModuleService { /** @@ -2198,6 +2200,20 @@ export interface IPricingModuleService { sharedContext?: Context ): Promise<[PriceSetMoneyAmountRulesDTO[], number]> + + listPriceSetMoneyAmounts( + filters?: FilterablePriceSetMoneyAmountProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCountPriceSetMoneyAmounts( + filters?: FilterablePriceSetMoneyAmountProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[PriceSetMoneyAmountDTO[], number]> + + /** * This method is used to create new price set money amount rules. A price set money amount rule creates an association between a price set money amount and * a rule type. diff --git a/packages/types/src/workflow/product/index.ts b/packages/types/src/workflow/product/index.ts index 673206ae94..f960c39643 100644 --- a/packages/types/src/workflow/product/index.ts +++ b/packages/types/src/workflow/product/index.ts @@ -1,2 +1,3 @@ export * from "./create-products" +export * from "./update-product-variants" export * from "./update-products" diff --git a/packages/types/src/workflow/product/update-product-variants.ts b/packages/types/src/workflow/product/update-product-variants.ts new file mode 100644 index 0000000000..693eeeb86e --- /dev/null +++ b/packages/types/src/workflow/product/update-product-variants.ts @@ -0,0 +1,41 @@ +export interface UpsertProductVariantPricesInputDTO { + id?: string + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + +export interface UpsertProductVariantOptionInputDTO { + value: string + option_id: string +} + +export interface UpdateProductVariantsInputDTO { + 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 UpdateProductVariantsWorkflowInputDTO { + productVariants: UpdateProductVariantsInputDTO[] +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index af925d5d85..a0e136c6b7 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -18,6 +18,7 @@ export * from "./object-from-string-path" export * from "./object-to-string-path" export * from "./remote-query-object-from-string" export * from "./remote-query-object-to-string" +export * from './remove-nullisih' export * from "./set-metadata" export * from "./simple-hash" export * from "./string-to-select-relation-object" diff --git a/packages/utils/src/common/remove-nullisih.ts b/packages/utils/src/common/remove-nullisih.ts new file mode 100644 index 0000000000..5019239d80 --- /dev/null +++ b/packages/utils/src/common/remove-nullisih.ts @@ -0,0 +1,14 @@ +import { isDefined } from "./is-defined" + +export const removeNullish = ( + obj: Record +): Record => + Object.entries(obj).reduce((resultObject, [currentKey, currentValue]) => { + if (!isDefined(currentValue) || currentValue === null) { + return resultObject + } + + resultObject[currentKey] = currentValue + + return resultObject + }, {}) diff --git a/packages/utils/src/modules-sdk/load-module-database-config.ts b/packages/utils/src/modules-sdk/load-module-database-config.ts index baed3cd977..c788c8cbd7 100644 --- a/packages/utils/src/modules-sdk/load-module-database-config.ts +++ b/packages/utils/src/modules-sdk/load-module-database-config.ts @@ -1,5 +1,5 @@ -import { ModulesSdkTypes } from "@medusajs/types" import { MedusaError } from "../common" +import { ModulesSdkTypes } from "@medusajs/types" function getEnv(key: string, moduleName: string): string { const value = @@ -66,7 +66,7 @@ export function loadDatabaseConfig( "clientUrl" | "schema" | "driverOptions" | "debug" > { const clientUrl = getEnv("POSTGRES_URL", moduleName) - + const database = { clientUrl, schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public", diff --git a/packages/workflows/src/definition/product/index.ts b/packages/workflows/src/definition/product/index.ts index 673206ae94..ab2a40334d 100644 --- a/packages/workflows/src/definition/product/index.ts +++ b/packages/workflows/src/definition/product/index.ts @@ -1,2 +1,3 @@ export * from "./create-products" +export * as UpdateProductVariants from "./update-product-variants" export * from "./update-products" diff --git a/packages/workflows/src/definition/product/update-product-variants.ts b/packages/workflows/src/definition/product/update-product-variants.ts new file mode 100644 index 0000000000..cf5e54d7de --- /dev/null +++ b/packages/workflows/src/definition/product/update-product-variants.ts @@ -0,0 +1,108 @@ +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 UpdateProductVariantsActions { + prepare = "prepare", + updateProductVariants = "updateProductVariants", + revertProductVariantsUpdate = "revertProductVariantsUpdate", + upsertPrices = "upsertPrices", +} + +export const workflowSteps: TransactionStepsDefinition = { + next: { + action: UpdateProductVariantsActions.prepare, + noCompensation: true, + next: { + action: UpdateProductVariantsActions.updateProductVariants, + noCompensation: true, + next: [ + { + action: UpdateProductVariantsActions.upsertPrices, + }, + ], + }, + }, +} + +const handlers = new Map([ + [ + UpdateProductVariantsActions.prepare, + { + invoke: pipe( + { + merge: true, + inputAlias: InputAlias.ProductVariantsUpdateInputData, + invoke: { + from: InputAlias.ProductVariantsUpdateInputData, + }, + }, + ProductHandlers.updateProductVariantsPrepareData + ), + }, + ], + [ + UpdateProductVariantsActions.updateProductVariants, + { + invoke: pipe( + { + merge: true, + invoke: { + from: UpdateProductVariantsActions.prepare, + }, + }, + ProductHandlers.updateProductVariants + ), + }, + ], + [ + UpdateProductVariantsActions.upsertPrices, + { + invoke: pipe( + { + merge: true, + invoke: [ + { + from: UpdateProductVariantsActions.prepare, + }, + ], + }, + ProductHandlers.upsertVariantPrices + ), + compensate: pipe( + { + merge: true, + invoke: [ + { + from: UpdateProductVariantsActions.prepare, + }, + { + from: UpdateProductVariantsActions.upsertPrices, + }, + ], + }, + ProductHandlers.revertVariantPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.UpdateProductVariants, + workflowSteps, + handlers +) + +export const updateProductVariants = exportWorkflow< + WorkflowTypes.ProductWorkflow.UpdateProductVariantsWorkflowInputDTO, + ProductTypes.ProductVariantDTO[] +>( + Workflows.UpdateProductVariants, + UpdateProductVariantsActions.updateProductVariants +) diff --git a/packages/workflows/src/definition/product/update-products.ts b/packages/workflows/src/definition/product/update-products.ts index f0b0bdfc98..3c749a412e 100644 --- a/packages/workflows/src/definition/product/update-products.ts +++ b/packages/workflows/src/definition/product/update-products.ts @@ -1,16 +1,17 @@ import { ProductTypes, WorkflowTypes } from "@medusajs/types" -import { InputAlias, Workflows } from "../../definitions" import { TransactionStepsDefinition, WorkflowManager, } from "@medusajs/orchestration" -import { exportWorkflow, pipe } from "../../helper" -import { CreateProductsActions } from "./create-products" +import { InputAlias, Workflows } from "../../definitions" import { InventoryHandlers, ProductHandlers } from "../../handlers" import * as MiddlewareHandlers from "../../handlers/middlewares" import { detachSalesChannelFromProducts } from "../../handlers/product" +import { exportWorkflow, pipe } from "../../helper" +import { CreateProductsActions } from "./create-products" import { prepareCreateInventoryItems } from "./prepare-create-inventory-items" +import { UpdateProductVariantsActions } from "./update-product-variants" export enum UpdateProductsActions { prepare = "prepare", @@ -32,6 +33,10 @@ export const updateProductsWorkflowSteps: TransactionStepsDefinition = { next: { action: UpdateProductsActions.updateProducts, next: [ + { + action: UpdateProductVariantsActions.upsertPrices, + saveResponse: false, + }, { action: UpdateProductsActions.attachSalesChannels, saveResponse: false, @@ -59,7 +64,7 @@ export const updateProductsWorkflowSteps: TransactionStepsDefinition = { }, } -const handlers = new Map([ +const handlers = new Map([ [ UpdateProductsActions.prepare, { @@ -350,6 +355,37 @@ const handlers = new Map([ ), }, ], + [ + UpdateProductVariantsActions.upsertPrices, + { + invoke: pipe( + { + merge: true, + invoke: [ + { + from: InputAlias.ProductsInputData, + alias: ProductHandlers.updateProducts.aliases.products, + }, + { + from: UpdateProductsActions.prepare, + }, + ], + }, + ProductHandlers.upsertVariantPrices + ), + compensate: pipe( + { + merge: true, + invoke: [ + { + from: UpdateProductVariantsActions.upsertPrices, + }, + ], + }, + ProductHandlers.revertVariantPrices + ), + }, + ], ]) WorkflowManager.register( diff --git a/packages/workflows/src/definitions.ts b/packages/workflows/src/definitions.ts index e4b98994cb..5f4fa84616 100644 --- a/packages/workflows/src/definitions.ts +++ b/packages/workflows/src/definitions.ts @@ -3,6 +3,9 @@ export enum Workflows { CreateProducts = "create-products", UpdateProducts = "update-products", + // Product Variant workflows + UpdateProductVariants = "update-product-variants", + // Cart workflows CreateCart = "create-cart", @@ -14,6 +17,9 @@ export enum InputAlias { ProductsInputData = "productsInputData", RemovedProducts = "removedProducts", + ProductVariants = "productVariants", + ProductVariantsUpdateInputData = "productVariantsUpdateInputData", + InventoryItems = "inventoryItems", RemovedInventoryItems = "removedInventoryItems", 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 608b2ccc9f..b6b2f1c0c6 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, - kebabCase, ShippingProfileUtils, + kebabCase, } from "@medusajs/utils" import { WorkflowArguments } from "../../helper" diff --git a/packages/workflows/src/handlers/product/index.ts b/packages/workflows/src/handlers/product/index.ts index d15f60321a..697c0e0b83 100644 --- a/packages/workflows/src/handlers/product/index.ts +++ b/packages/workflows/src/handlers/product/index.ts @@ -1,12 +1,16 @@ -export * from "./create-products-prepare-data" -export * from "./create-products" -export * from "./detach-sales-channel-from-products" export * from "./attach-sales-channel-to-products" -export * from "./detach-shipping-profile-from-products" -export * from "./remove-products" export * from "./attach-shipping-profile-to-products" +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-products" +export * from "./revert-update-products" +export * from "./revert-variant-prices" +export * from "./update-product-variants" +export * from "./update-product-variants-prepare-data" export * from "./update-products" export * from "./update-products-prepare-data" -export * from "./revert-update-products" export * from "./update-products-variants-prices" +export * from "./upsert-variant-prices" diff --git a/packages/workflows/src/handlers/product/revert-variant-prices.ts b/packages/workflows/src/handlers/product/revert-variant-prices.ts new file mode 100644 index 0000000000..2fa0a72824 --- /dev/null +++ b/packages/workflows/src/handlers/product/revert-variant-prices.ts @@ -0,0 +1,48 @@ +import { PricingTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type HandlerInput = { + createdLinks: Record[] + originalMoneyAmounts: PricingTypes.MoneyAmountDTO[] + createdPriceSets: PricingTypes.PriceSetDTO[] +} + +export async function revertVariantPrices({ + container, + context, + data, +}: WorkflowArguments): Promise { + const { + createdLinks = [], + originalMoneyAmounts = [], + createdPriceSets = [], + } = data + + const featureFlagRouter = container.resolve("featureFlagRouter") + const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled( + "isolate_pricing_domain" + ) + + if (!isPricingDomainEnabled) { + return + } + + const pricingModuleService = container.resolve("pricingModuleService") + const remoteLink = container.resolve("remoteLink") + + await remoteLink.remove(createdLinks) + + if (originalMoneyAmounts.length) { + await pricingModuleService.updateMoneyAmounts(originalMoneyAmounts) + } + + if (createdPriceSets.length) { + await pricingModuleService.delete({ + id: createdPriceSets.map((cps) => cps.id), + }) + } +} + +revertVariantPrices.aliases = { + productVariantsPrices: "productVariantsPrices", +} diff --git a/packages/workflows/src/handlers/product/update-product-variants-prepare-data.ts b/packages/workflows/src/handlers/product/update-product-variants-prepare-data.ts new file mode 100644 index 0000000000..40fc8d7961 --- /dev/null +++ b/packages/workflows/src/handlers/product/update-product-variants-prepare-data.ts @@ -0,0 +1,95 @@ +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { ProductTypes, 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 UpdateProductVariantsPreparedData = { + productVariants: ProductWorkflow.UpdateProductVariantsInputDTO[] + variantPricesMap: Map + productVariantsMap: Map< + string, + ProductWorkflow.UpdateProductVariantsInputDTO[] + > +} + +export async function updateProductVariantsPrepareData({ + container, + context, + data, +}: WorkflowArguments): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled( + "isolate_pricing_domain" + ) + let productVariants: ProductWorkflow.UpdateProductVariantsInputDTO[] = + data.productVariants || [] + + const variantsDataMap = new Map< + string, + ProductWorkflow.UpdateProductVariantsInputDTO + >( + productVariants.map((productVariantData) => [ + productVariantData.id, + productVariantData, + ]) + ) + + const variantIds = productVariants.map((pv) => pv.id) as string[] + const productVariantsMap = new Map< + string, + ProductWorkflow.UpdateProductVariantsInputDTO[] + >() + const variantPricesMap = new Map() + + const productModuleService: ProductTypes.IProductModuleService = + container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) + + const variantsWithProductIds = await productModuleService.listVariants( + { + id: variantIds, + }, + { + select: ["id", "product_id"], + } + ) + + for (const variantWithProductID of variantsWithProductIds) { + const variantData = variantsDataMap.get(variantWithProductID.id) + + if (!variantData) { + continue + } + + variantPricesMap.set(variantWithProductID.id, variantData.prices || []) + if (isPricingDomainEnabled) { + delete variantData.prices + } + + const variantsData: ProductWorkflow.UpdateProductVariantsInputDTO[] = + productVariantsMap.get(variantWithProductID.product_id) || [] + + if (variantData) { + variantsData.push(variantData) + } + + productVariantsMap.set(variantWithProductID.product_id, variantsData) + } + + return { + productVariants, + variantPricesMap, + productVariantsMap, + } +} + +updateProductVariantsPrepareData.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/product/update-product-variants.ts b/packages/workflows/src/handlers/product/update-product-variants.ts new file mode 100644 index 0000000000..3f057c91f1 --- /dev/null +++ b/packages/workflows/src/handlers/product/update-product-variants.ts @@ -0,0 +1,39 @@ +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type HandlerInput = { + productVariantsMap: Map +} + +export async function updateProductVariants({ + container, + data, +}: WorkflowArguments): Promise< + ProductTypes.UpdateProductVariantDTO[] +> { + const { productVariantsMap } = data + const productsVariants: ProductTypes.UpdateProductVariantDTO[] = [] + const updateProductsData: ProductTypes.UpdateProductDTO[] = [] + const productModuleService: ProductTypes.IProductModuleService = + container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) + + for (const [productId, variantsData = []] of productVariantsMap) { + updateProductsData.push({ + id: productId, + variants: variantsData, + }) + + productsVariants.push(...variantsData) + } + + if (updateProductsData.length) { + await productModuleService.update(updateProductsData) + } + + return productsVariants +} + +updateProductVariants.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/product/update-products-prepare-data.ts b/packages/workflows/src/handlers/product/update-products-prepare-data.ts index 6d0d914b3e..00d6186521 100644 --- a/packages/workflows/src/handlers/product/update-products-prepare-data.ts +++ b/packages/workflows/src/handlers/product/update-products-prepare-data.ts @@ -6,10 +6,19 @@ type ProductWithSalesChannelsDTO = ProductDTO & { sales_channels?: SalesChannelDTO[] } +type VariantPrice = { + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + export type UpdateProductsPreparedData = { originalProducts: ProductWithSalesChannelsDTO[] productHandleAddedChannelsMap: Map productHandleRemovedChannelsMap: Map + variantPricesMap: Map } export async function updateProductsPrepareData({ @@ -17,6 +26,12 @@ export async function updateProductsPrepareData({ context, data, }: WorkflowArguments): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled( + "isolate_pricing_domain" + ) + + const variantPricesMap = new Map() const ids = data.products.map((product) => product.id) const productHandleAddedChannelsMap = new Map() @@ -65,6 +80,16 @@ export async function updateProductsPrepareData({ }) } + for (const variantInput of productInput.variants || []) { + if (variantInput.id) { + variantPricesMap.set(variantInput.id, variantInput.prices || []) + } + + if (isPricingDomainEnabled) { + delete variantInput.prices + } + } + productHandleAddedChannelsMap.set(currentProduct.handle!, addedChannels) productHandleRemovedChannelsMap.set(currentProduct.handle!, removedChannels) }) @@ -73,6 +98,7 @@ export async function updateProductsPrepareData({ originalProducts: products, productHandleAddedChannelsMap, productHandleRemovedChannelsMap, + variantPricesMap, } } diff --git a/packages/workflows/src/handlers/product/update-products-variants-prices.ts b/packages/workflows/src/handlers/product/update-products-variants-prices.ts index a7dc754184..9934e645fd 100644 --- a/packages/workflows/src/handlers/product/update-products-variants-prices.ts +++ b/packages/workflows/src/handlers/product/update-products-variants-prices.ts @@ -1,4 +1,5 @@ import { ProductTypes, WorkflowTypes } from "@medusajs/types" + import { MedusaError } from "@medusajs/utils" import { WorkflowArguments } from "../../helper" @@ -26,9 +27,12 @@ export async function updateProductsVariantsPrices({ data.productsHandleVariantsIndexPricesMap const productVariantService = container.resolve("productVariantService") + const regionService = container.resolve("regionService") + const featureFlagRouter = container.resolve("featureFlagRouter") const productVariantServiceTx = productVariantService.withTransaction(manager) - const variantIdsPricesData: any[] = [] + const variantPricesMap = new Map() + const productsMap = new Map( products.map((p) => [p.handle!, p]) ) @@ -50,10 +54,52 @@ export async function updateProductsVariantsPrices({ variantId: variant.id, prices: item.prices, }) + + variantPricesMap.set(variant.id, []) + + item.prices.forEach(async (price) => { + const obj = { + amount: price.amount, + currency_code: price.currency_code, + rules: {}, + } + + if (price.region_id) { + const region = await regionService.retrieve(price.region_id) + obj.currency_code = region.currency_code + obj.rules = { + region_id: price.region_id, + } + } + + const variantPrices = variantPricesMap.get(variant.id) + variantPrices?.push(obj) + }) }) } - await productVariantServiceTx.updateVariantPrices(variantIdsPricesData) + if (featureFlagRouter.isFeatureEnabled("isolate_pricing_domain")) { + const remoteLink = container.resolve("remoteLink") + const pricingModuleService = container.resolve("pricingModuleService") + + for (let { variantId } of variantIdsPricesData) { + const priceSet = await pricingModuleService.create({ + rules: [{ rule_attribute: "region_id" }], + prices: variantPricesMap.get(variantId), + }) + + await remoteLink.create({ + productService: { + variant_id: variantId, + }, + pricingService: { + price_set_id: priceSet.id, + }, + }) + } + } else { + await productVariantServiceTx.updateVariantPrices(variantIdsPricesData) + } } updateProductsVariantsPrices.aliases = { diff --git a/packages/workflows/src/handlers/product/upsert-variant-prices.ts b/packages/workflows/src/handlers/product/upsert-variant-prices.ts new file mode 100644 index 0000000000..050a7d762f --- /dev/null +++ b/packages/workflows/src/handlers/product/upsert-variant-prices.ts @@ -0,0 +1,168 @@ +import { PricingTypes } from "@medusajs/types" + +import { WorkflowArguments } from "../../helper" + +type VariantPrice = { + id?: string + region_id?: string + currency_code: string + amount: number + min_quantity?: number + max_quantity?: number + rules: Record +} + +type RegionDTO = { + id: string + currency_code: string +} + +type HandlerInput = { + variantPricesMap: Map +} + +export async function upsertVariantPrices({ + container, + context, + data, +}: WorkflowArguments) { + const { variantPricesMap } = data + + const featureFlagRouter = container.resolve("featureFlagRouter") + + if (!featureFlagRouter.isFeatureEnabled("isolate_pricing_domain")) { + return { + createdLinks: [], + originalMoneyAmounts: [], + createdPriceSets: [], + } + } + + const pricingModuleService = container.resolve("pricingModuleService") + const regionService = container.resolve("regionService") + const remoteLink = container.resolve("remoteLink") + const remoteQuery = container.resolve("remoteQuery") + + const variables = { + variant_id: [...variantPricesMap.keys()], + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + + const variantIdToPriceSetIdMap: Map = new Map( + variantPriceSets.map((variantPriceSet) => [ + variantPriceSet.variant_id, + variantPriceSet.price_set_id, + ]) + ) + + const moneyAmountsToUpdate: PricingTypes.UpdateMoneyAmountDTO[] = [] + const createdPriceSets: PricingTypes.PriceSetDTO[] = [] + const ruleSetPricesToAdd: PricingTypes.CreatePricesDTO[] = [] + const linksToCreate: any[] = [] + + for (const [variantId, prices = []] of variantPricesMap) { + const priceSetToCreate: PricingTypes.CreatePriceSetDTO = { + rules: [{ rule_attribute: "region_id" }], + prices: [], + } + const regionIds = prices.map((price) => price.region_id) + const regions = await regionService.list({ id: regionIds }) + const regionsMap: Map = new Map( + regions.map((region: RegionDTO) => [region.id, region]) + ) + + for (const price of prices) { + if (price.id) { + moneyAmountsToUpdate.push({ + id: price.id, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + amount: price.amount, + currency_code: price.currency_code, + }) + } else { + const region = price.region_id && regionsMap.get(price.region_id) + const variantPrice: PricingTypes.CreatePricesDTO = { + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + amount: price.amount, + currency_code: price.currency_code, + rules: {}, + } + + if (region) { + variantPrice.currency_code = region.currency_code + variantPrice.rules = { + region_id: region.id, + } + } + + delete price.region_id + + if (variantIdToPriceSetIdMap.get(variantId)) { + ruleSetPricesToAdd.push(variantPrice) + } else { + priceSetToCreate.prices?.push(variantPrice) + } + } + } + + let priceSetId = variantIdToPriceSetIdMap.get(variantId) + + if (priceSetId) { + await pricingModuleService.addPrices({ + priceSetId, + prices: ruleSetPricesToAdd, + }) + } else { + const createdPriceSet = await pricingModuleService.create( + priceSetToCreate + ) + priceSetId = createdPriceSet?.id + + createdPriceSets.push(createdPriceSet) + } + + linksToCreate.push({ + productService: { + variant_id: variantId, + }, + pricingService: { + price_set_id: priceSetId, + }, + }) + } + + const createdLinks = await remoteLink.create(linksToCreate) + + let originalMoneyAmounts = await pricingModuleService.listMoneyAmounts( + { + id: moneyAmountsToUpdate.map((matu) => matu.id), + }, + { + select: ["id", "currency_code", "amount", "min_quantity", "max_quantity"], + } + ) + + if (moneyAmountsToUpdate.length) { + await pricingModuleService.updateMoneyAmounts(moneyAmountsToUpdate) + } + + return { + createdLinks, + originalMoneyAmounts, + createdPriceSets, + } +} + +upsertVariantPrices.aliases = { + productVariantsPrices: "productVariantsPrices", +} diff --git a/yarn.lock b/yarn.lock index 9f4ddffccf..6a1ccc7b78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6705,7 +6705,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/modules-sdk@^1.12.0, @medusajs/modules-sdk@^1.12.1, @medusajs/modules-sdk@^1.8.8, @medusajs/modules-sdk@workspace:packages/modules-sdk": +"@medusajs/modules-sdk@^1.12.0, @medusajs/modules-sdk@^1.12.1, @medusajs/modules-sdk@^1.8.8, @medusajs/modules-sdk@workspace:^, @medusajs/modules-sdk@workspace:packages/modules-sdk": version: 0.0.0-use.local resolution: "@medusajs/modules-sdk@workspace:packages/modules-sdk" dependencies: @@ -6788,7 +6788,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/pricing@workspace:packages/pricing": +"@medusajs/pricing@workspace:^, @medusajs/pricing@workspace:packages/pricing": version: 0.0.0-use.local resolution: "@medusajs/pricing@workspace:packages/pricing" dependencies: @@ -26517,6 +26517,8 @@ __metadata: "@medusajs/event-bus-local": "workspace:*" "@medusajs/inventory": "workspace:^" "@medusajs/medusa": "workspace:*" + "@medusajs/modules-sdk": "workspace:^" + "@medusajs/pricing": "workspace:^" "@medusajs/product": "workspace:^" babel-preset-medusa-package: "*" faker: ^5.5.3