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 <rmthamir@gmail.com>

* 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 <philip.korsholm@hotmail.com>

* 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 <rmthamir@gmail.com>
This commit is contained in:
Philip Korsholm
2023-10-30 14:42:17 +01:00
committed by GitHub
parent b69f182571
commit 148f537b47
84 changed files with 2702 additions and 284 deletions

View File

@@ -21,7 +21,7 @@ const {
} = require("../../../../factories")
const setupServer = require("../../../../environment-helpers/setup-server")
jest.setTimeout(30000)
jest.setTimeout(100000)
const adminHeaders = {
headers: {

View File

@@ -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()

View File

@@ -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",

View File

@@ -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({

View File

@@ -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()
}
})
})
}

View File

@@ -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()

View File

@@ -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_
},
}

View File

@@ -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_
},
}

View File

@@ -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 () {

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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",
}),
])
})
})
})

View File

@@ -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",
}),
]),
}),
]),
}),
})
})
})

View File

@@ -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 () {

View File

@@ -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",
}),
]),
}),
]),
})
)
})
})

View File

@@ -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",
}),
]),
}),
]),
})
)
})
})

View File

@@ -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

View File

@@ -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",
},
])
}

View File

@@ -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<PriceSetDTO> => {
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"],
})
}

View File

@@ -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",
},
},
}

View File

@@ -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:*",