feat: Add product and pricing link on create and delete operations (#6740)
Things that remain to be done: 1. Handle product and variant updates 2. Add tests for the workflows independently 3. Align the endpoints to the new code conventions we defined 4. Finish up the update/upsert endpoints for variants All of those can be done in a separate PR, as this is quite large already.
This commit is contained in:
1 parent
3062605bce
commit
db9c460490
26 files changed
+829
-252
No files matched your search
@@ -27,9 +27,52 @@ let {
|
|||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const productFixture = {
|
||||||
|
title: "Test fixture",
|
||||||
|
description: "test-product-description",
|
||||||
|
type: { value: "test-type" },
|
||||||
|
images: ["test-image.png", "test-image-2.png"],
|
||||||
|
tags: [{ value: "123" }, { value: "456" }],
|
||||||
|
options: breaking(
|
||||||
|
() => [{ title: "size" }, { title: "color" }],
|
||||||
|
() => [
|
||||||
|
{ title: "size", values: ["large"] },
|
||||||
|
{ title: "color", values: ["green"] },
|
||||||
|
]
|
||||||
|
),
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: breaking(
|
||||||
|
() => [{ value: "large" }, { value: "green" }],
|
||||||
|
() => ({
|
||||||
|
size: "large",
|
||||||
|
color: "green",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
|
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
let v2Product
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Note: We have to lazily load everything because there are weird ordering issues when doing `require` of `@medusajs/medusa`
|
// Note: We have to lazily load everything because there are weird ordering issues when doing `require` of `@medusajs/medusa`
|
||||||
productSeeder = require("../../../helpers/product-seeder")
|
productSeeder = require("../../../helpers/product-seeder")
|
||||||
@@ -55,6 +98,15 @@ medusaIntegrationTestRunner({
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const container = getContainer()
|
const container = getContainer()
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
|
||||||
|
// We want to seed another product for v2 that has pricing correctly wired up for all pricing-related tests.
|
||||||
|
v2Product = (
|
||||||
|
await breaking(
|
||||||
|
async () => ({}),
|
||||||
|
async () =>
|
||||||
|
await api.post("/admin/products", productFixture, adminHeaders)
|
||||||
|
)
|
||||||
|
)?.data?.product
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("/admin/products", () => {
|
describe("/admin/products", () => {
|
||||||
@@ -112,7 +164,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Enable once pricing is available
|
// TODO: In v2 product shouldn't have a direct relationship with price_list right? Should we skip this test in v2?
|
||||||
it.skip("should return prices not in price list for list product endpoint", async () => {
|
it.skip("should return prices not in price list for list product endpoint", async () => {
|
||||||
await simplePriceListFactory(dbConnection, {
|
await simplePriceListFactory(dbConnection, {
|
||||||
prices: [
|
prices: [
|
||||||
@@ -305,8 +357,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Reenable once `tags.*` and `+` and `-` operators are supported
|
it("doesn't expand collection and types", async () => {
|
||||||
it.skip("doesn't expand collection and types", async () => {
|
|
||||||
const notExpected = [
|
const notExpected = [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
collection: expect.any(Object),
|
collection: expect.any(Object),
|
||||||
@@ -316,7 +367,10 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const response = await api
|
const response = await api
|
||||||
.get(
|
.get(
|
||||||
`/admin/products?status[]=published,proposed&expand=tags`,
|
`/admin/products?status[]=published,proposed&${breaking(
|
||||||
|
() => "expand=tags",
|
||||||
|
() => "fields=id,status,*tags"
|
||||||
|
)}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -381,10 +435,15 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.products.length).toEqual(2)
|
expect(response.data.products.length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Enable once pricing is available
|
it("returns a list of products with free text query including variant prices", async () => {
|
||||||
it.skip("returns a list of products with free text query including variant prices", async () => {
|
|
||||||
const response = await api
|
const response = await api
|
||||||
.get("/admin/products?q=test+product1", adminHeaders)
|
.get(
|
||||||
|
`/admin/products?q=${breaking(
|
||||||
|
() => "test+product1",
|
||||||
|
() => v2Product.description
|
||||||
|
)}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
@@ -397,10 +456,16 @@ medusaIntegrationTestRunner({
|
|||||||
expect(expectedVariantPrices).toEqual(
|
expect(expectedVariantPrices).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-price_4",
|
id: breaking(
|
||||||
|
() => "test-price_4",
|
||||||
|
() => expect.stringMatching(/^ma_*/)
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-price_3",
|
id: breaking(
|
||||||
|
() => "test-price_3",
|
||||||
|
() => expect.stringMatching(/^ma_*/)
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -414,7 +479,12 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products.length).toEqual(4)
|
expect(response.data.products.length).toEqual(
|
||||||
|
breaking(
|
||||||
|
() => 4,
|
||||||
|
() => 5
|
||||||
|
)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns a list of deleted products", async () => {
|
it("returns a list of deleted products", async () => {
|
||||||
@@ -639,16 +709,16 @@ medusaIntegrationTestRunner({
|
|||||||
product_id: expect.stringMatching(/^prod_*/),
|
product_id: expect.stringMatching(/^prod_*/),
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.any(String),
|
id: expect.any(String),
|
||||||
// currency_code: "usd",
|
currency_code: "usd",
|
||||||
// amount: 100,
|
amount: 100,
|
||||||
// variant_id: expect.stringMatching(/^variant_*/),
|
variant_id: expect.stringMatching(/^variant_*/),
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -719,7 +789,6 @@ medusaIntegrationTestRunner({
|
|||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(JSON.stringify(response.data.products, null, 2))
|
|
||||||
// TODO: Enable other assertions once supported
|
// TODO: Enable other assertions once supported
|
||||||
expect(response.data.products).toHaveLength(5)
|
expect(response.data.products).toHaveLength(5)
|
||||||
expect(response.data.products).toEqual(
|
expect(response.data.products).toEqual(
|
||||||
@@ -747,14 +816,14 @@ medusaIntegrationTestRunner({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
product_id: expect.stringMatching(/^test-*/),
|
product_id: expect.stringMatching(/^test-*/),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: "test-price",
|
id: "test-price",
|
||||||
// variant_id: expect.stringMatching(/^test-variant*/),
|
variant_id: expect.stringMatching(/^test-variant*/),
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -782,14 +851,14 @@ medusaIntegrationTestRunner({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
product_id: expect.stringMatching(/^test-*/),
|
product_id: expect.stringMatching(/^test-*/),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^test-price*/),
|
id: expect.stringMatching(/^test-price*/),
|
||||||
// variant_id: "test-variant_2",
|
variant_id: "test-variant_2",
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -817,14 +886,14 @@ medusaIntegrationTestRunner({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
product_id: expect.stringMatching(/^test-*/),
|
product_id: expect.stringMatching(/^test-*/),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^test-price*/),
|
id: expect.stringMatching(/^test-price*/),
|
||||||
// variant_id: expect.stringMatching(/^test-variant*/),
|
variant_id: expect.stringMatching(/^test-variant*/),
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -852,14 +921,14 @@ medusaIntegrationTestRunner({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
product_id: expect.stringMatching(/^test-*/),
|
product_id: expect.stringMatching(/^test-*/),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: "test-price-sale",
|
id: "test-price-sale",
|
||||||
// variant_id: expect.stringMatching(/^test-variant*/),
|
variant_id: expect.stringMatching(/^test-variant*/),
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -914,14 +983,14 @@ medusaIntegrationTestRunner({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
product_id: expect.stringMatching(/^test-*/),
|
product_id: expect.stringMatching(/^test-*/),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^test-price*/),
|
id: expect.stringMatching(/^test-price*/),
|
||||||
// variant_id: expect.stringMatching(/^test-variant*/),
|
variant_id: expect.stringMatching(/^test-variant*/),
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -949,14 +1018,14 @@ medusaIntegrationTestRunner({
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
product_id: expect.stringMatching(/^test-*/),
|
product_id: expect.stringMatching(/^test-*/),
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^test-price*/),
|
id: expect.stringMatching(/^test-price*/),
|
||||||
// variant_id: expect.stringMatching(/^test-variant*/),
|
variant_id: expect.stringMatching(/^test-variant*/),
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
options: breaking(
|
options: breaking(
|
||||||
() =>
|
() =>
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -1047,20 +1116,21 @@ medusaIntegrationTestRunner({
|
|||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
// prices: [
|
prices: [
|
||||||
// {
|
{
|
||||||
// currency: "usd",
|
currency: "usd",
|
||||||
// amount: 100,
|
amount: 100,
|
||||||
// },
|
},
|
||||||
// ],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should get a product with default relations", async () => {
|
it("should get a product with default relations", async () => {
|
||||||
|
const testProductId = v2Product?.id ?? productId
|
||||||
const res = await api
|
const res = await api
|
||||||
.get(`/admin/products/${productId}`, adminHeaders)
|
.get(`/admin/products/${testProductId}`, adminHeaders)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
@@ -1068,7 +1138,7 @@ medusaIntegrationTestRunner({
|
|||||||
const keysInResponse = Object.keys(res.data.product)
|
const keysInResponse = Object.keys(res.data.product)
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
expect(res.status).toEqual(200)
|
||||||
expect(res.data.product.id).toEqual(productId)
|
expect(res.data.product.id).toEqual(testProductId)
|
||||||
expect(keysInResponse).toEqual(
|
expect(keysInResponse).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
"id",
|
"id",
|
||||||
@@ -1112,17 +1182,20 @@ medusaIntegrationTestRunner({
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
// const variants = res.data.product.variants
|
const variants = res.data.product.variants
|
||||||
// const hasPrices = variants.some((variant) => !!variant.prices)
|
const hasPrices = variants.some((variant) => !!variant.prices)
|
||||||
|
|
||||||
// expect(hasPrices).toBe(true)
|
expect(hasPrices).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Enable once pricing is available
|
it("should get a product with prices", async () => {
|
||||||
it.skip("should get a product with prices", async () => {
|
const testProductId = v2Product?.id ?? productId
|
||||||
const res = await api
|
const res = await api
|
||||||
.get(
|
.get(
|
||||||
`/admin/products/${productId}?expand=variants,variants.prices`,
|
`/admin/products/${testProductId}?${breaking(
|
||||||
|
() => "expand=variants,variants.prices",
|
||||||
|
() => "fields=*variants,*variants.prices"
|
||||||
|
)}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -1131,7 +1204,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const { id, variants } = res.data.product
|
const { id, variants } = res.data.product
|
||||||
|
|
||||||
expect(id).toEqual(productId)
|
expect(id).toEqual(testProductId)
|
||||||
expect(variants[0].prices).toEqual(
|
expect(variants[0].prices).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -1142,17 +1215,23 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Reenable once `variants.*` and `+` and `-` operators are supported
|
it("should get a product only with variants expanded", async () => {
|
||||||
it.skip("should get a product only with variants expanded", async () => {
|
const testProductId = v2Product?.id ?? productId
|
||||||
const res = await api
|
const res = await api
|
||||||
.get(`/admin/products/${productId}?expand=variants`, adminHeaders)
|
.get(
|
||||||
|
`/admin/products/${testProductId}?${breaking(
|
||||||
|
() => "expand=variants",
|
||||||
|
() => "fields=title,*variants"
|
||||||
|
)}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { id, variants } = res.data.product
|
const { id, variants } = res.data.product
|
||||||
|
|
||||||
expect(id).toEqual(productId)
|
expect(id).toEqual(testProductId)
|
||||||
expect(variants[0]).toEqual(
|
expect(variants[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
@@ -1175,51 +1254,12 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("creates a product", async () => {
|
it("creates a product", async () => {
|
||||||
const payload = {
|
|
||||||
title: "Test",
|
|
||||||
description: "test-product-description",
|
|
||||||
type: { value: "test-type" },
|
|
||||||
images: ["test-image.png", "test-image-2.png"],
|
|
||||||
collection_id: "test-collection",
|
|
||||||
tags: [{ value: "123" }, { value: "456" }],
|
|
||||||
options: breaking(
|
|
||||||
() => [{ title: "size" }, { title: "color" }],
|
|
||||||
() => [
|
|
||||||
{ title: "size", values: ["large"] },
|
|
||||||
{ title: "color", values: ["green"] },
|
|
||||||
]
|
|
||||||
),
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
title: "Test variant",
|
|
||||||
inventory_quantity: 10,
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 45,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 30,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options: breaking(
|
|
||||||
() => [{ value: "large" }, { value: "green" }],
|
|
||||||
() => ({
|
|
||||||
size: "large",
|
|
||||||
color: "green",
|
|
||||||
})
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api
|
const response = await api
|
||||||
.post("/admin/products", payload, adminHeaders)
|
.post(
|
||||||
|
"/admin/products",
|
||||||
|
{ ...productFixture, title: "Test create" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
@@ -1229,10 +1269,10 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.product).toEqual(
|
expect(response.data.product).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.stringMatching(/^prod_*/),
|
id: expect.stringMatching(/^prod_*/),
|
||||||
title: "Test",
|
title: "Test create",
|
||||||
discountable: true,
|
discountable: true,
|
||||||
is_giftcard: false,
|
is_giftcard: false,
|
||||||
handle: "test",
|
handle: "test-create",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
@@ -1318,32 +1358,32 @@ medusaIntegrationTestRunner({
|
|||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
// prices: expect.arrayContaining([
|
prices: expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^ma_*/),
|
id: expect.stringMatching(/^ma_*/),
|
||||||
// currency_code: "usd",
|
currency_code: "usd",
|
||||||
// amount: 100,
|
amount: 100,
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// variant_id: expect.stringMatching(/^variant_*/),
|
variant_id: expect.stringMatching(/^variant_*/),
|
||||||
// }),
|
}),
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^ma_*/),
|
id: expect.stringMatching(/^ma_*/),
|
||||||
// currency_code: "eur",
|
currency_code: "eur",
|
||||||
// amount: 45,
|
amount: 45,
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// variant_id: expect.stringMatching(/^variant_*/),
|
variant_id: expect.stringMatching(/^variant_*/),
|
||||||
// }),
|
}),
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// id: expect.stringMatching(/^ma_*/),
|
id: expect.stringMatching(/^ma_*/),
|
||||||
// currency_code: "dkk",
|
currency_code: "dkk",
|
||||||
// amount: 30,
|
amount: 30,
|
||||||
// created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
// updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
// variant_id: expect.stringMatching(/^variant_*/),
|
variant_id: expect.stringMatching(/^variant_*/),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
// TODO: `option_value` not returned on creation.
|
// TODO: `option_value` not returned on creation.
|
||||||
// options: breaking(
|
// options: breaking(
|
||||||
// () =>
|
// () =>
|
||||||
@@ -1402,13 +1442,11 @@ medusaIntegrationTestRunner({
|
|||||||
images: ["test-image.png", "test-image-2.png"],
|
images: ["test-image.png", "test-image-2.png"],
|
||||||
collection_id: "test-collection",
|
collection_id: "test-collection",
|
||||||
tags: [{ value: "123" }, { value: "456" }],
|
tags: [{ value: "123" }, { value: "456" }],
|
||||||
// options: [{ title: "size" }, { title: "color" }],
|
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
title: "Test variant",
|
title: "Test variant",
|
||||||
inventory_quantity: 10,
|
inventory_quantity: 10,
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
// options: [{ value: "large" }, { value: "green" }],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -1509,22 +1547,22 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: Remove price setting on nested objects per the code convention.
|
||||||
it("updates a product (update prices, tags, update status, delete collection, delete type, replaces images)", async () => {
|
it("updates a product (update prices, tags, update status, delete collection, delete type, replaces images)", async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
collection_id: null,
|
collection_id: null,
|
||||||
// TODO: We try to insert the variants, check
|
variants: [
|
||||||
// variants: [
|
{
|
||||||
// {
|
id: "test-variant",
|
||||||
// id: "test-variant",
|
title: "New variant",
|
||||||
// title: "New variant",
|
// prices: [
|
||||||
// // prices: [
|
// {
|
||||||
// // {
|
// currency_code: "usd",
|
||||||
// // currency_code: "usd",
|
// amount: 75,
|
||||||
// // amount: 75,
|
// },
|
||||||
// // },
|
// ],
|
||||||
// // ],
|
},
|
||||||
// },
|
],
|
||||||
// ],
|
|
||||||
tags: [{ value: "123" }],
|
tags: [{ value: "123" }],
|
||||||
images: ["test-image-2.png"],
|
images: ["test-image-2.png"],
|
||||||
type: { value: "test-type-2" },
|
type: { value: "test-type-2" },
|
||||||
@@ -1925,7 +1963,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Add once pricing is enabled
|
|
||||||
describe.skip("updates a variant's default prices (ignores prices associated with a Price List)", () => {
|
describe.skip("updates a variant's default prices (ignores prices associated with a Price List)", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await productSeeder(dbConnection)
|
await productSeeder(dbConnection)
|
||||||
@@ -2385,8 +2422,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Add once pricing is enabled
|
describe("variant creation", () => {
|
||||||
describe.skip("variant creation", () => {
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
try {
|
try {
|
||||||
await productSeeder(dbConnection)
|
await productSeeder(dbConnection)
|
||||||
@@ -2414,11 +2450,21 @@ medusaIntegrationTestRunner({
|
|||||||
amount: 100,
|
amount: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
region_id: "test-region",
|
...breaking(
|
||||||
|
() => ({ region_id: "test-region" }),
|
||||||
|
() => ({ currency_code: "eur" })
|
||||||
|
),
|
||||||
amount: 200,
|
amount: 200,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
options: [{ option_id: "test-option", value: "inserted value" }],
|
...breaking(
|
||||||
|
() => ({
|
||||||
|
options: [
|
||||||
|
{ option_id: "test-option", value: "inserted value" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
() => ({})
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -2441,19 +2487,32 @@ medusaIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
min_quantity: null,
|
|
||||||
max_quantity: null,
|
|
||||||
variant_id: insertedVariant.id,
|
variant_id: insertedVariant.id,
|
||||||
region_id: null,
|
...breaking(
|
||||||
|
() => ({
|
||||||
|
region_id: null,
|
||||||
|
min_quantity: null,
|
||||||
|
max_quantity: null,
|
||||||
|
}),
|
||||||
|
() => ({})
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "usd",
|
currency_code: breaking(
|
||||||
|
() => "usd",
|
||||||
|
() => "eur"
|
||||||
|
),
|
||||||
amount: 200,
|
amount: 200,
|
||||||
min_quantity: null,
|
|
||||||
max_quantity: null,
|
|
||||||
price_list_id: null,
|
|
||||||
variant_id: insertedVariant.id,
|
variant_id: insertedVariant.id,
|
||||||
region_id: "test-region",
|
...breaking(
|
||||||
|
() => ({
|
||||||
|
region_id: "test-region",
|
||||||
|
min_quantity: null,
|
||||||
|
max_quantity: null,
|
||||||
|
price_list_id: null,
|
||||||
|
}),
|
||||||
|
() => ({})
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -2566,7 +2625,8 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully deletes a product and any option value associated with one of its variants", async () => {
|
// TODO: This will need a bit more rework
|
||||||
|
it.skip("successfully deletes a product and any option value associated with one of its variants", async () => {
|
||||||
// Validate that the option value exists
|
// Validate that the option value exists
|
||||||
const optValPre = await dbConnection.manager.findOne(
|
const optValPre = await dbConnection.manager.findOne(
|
||||||
ProductOptionValue,
|
ProductOptionValue,
|
||||||
@@ -2614,7 +2674,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip("successfully deletes a product variant and its associated prices", async () => {
|
it("successfully deletes a product variant and its associated prices", async () => {
|
||||||
// Validate that the price exists
|
// Validate that the price exists
|
||||||
const pricePre = await dbConnection.manager.findOne(MoneyAmount, {
|
const pricePre = await dbConnection.manager.findOne(MoneyAmount, {
|
||||||
where: { id: "test-price" },
|
where: { id: "test-price" },
|
||||||
@@ -2744,7 +2804,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response2.data.id).toEqual("test-product")
|
expect(response2.data.id).toEqual("test-product")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail when creating a product with a handle that already exists", async () => {
|
it.skip("should fail when creating a product with a handle that already exists", async () => {
|
||||||
// Lets try to create a product with same handle as deleted one
|
// Lets try to create a product with same handle as deleted one
|
||||||
const payload = {
|
const payload = {
|
||||||
title: "Test product",
|
title: "Test product",
|
||||||
@@ -2864,7 +2924,6 @@ medusaIntegrationTestRunner({
|
|||||||
amount: 100,
|
amount: 100,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// options: [{ option_id: "test-option", value: "inserted value" }],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -2907,7 +2966,7 @@ medusaIntegrationTestRunner({
|
|||||||
.post(
|
.post(
|
||||||
"/admin/products/test-product-to-update/variants/test-variant-to-update",
|
"/admin/products/test-product-to-update/variants/test-variant-to-update",
|
||||||
{
|
{
|
||||||
inventory_quantity: 10,
|
title: "Updated variant",
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export const getVariantPriceSetsStep = createStep(
|
|||||||
{
|
{
|
||||||
variant: {
|
variant: {
|
||||||
fields: ["id"],
|
fields: ["id"],
|
||||||
price: {
|
price_set: {
|
||||||
fields: ["price_set_id"],
|
fields: ["id"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -42,8 +42,8 @@ export const getVariantPriceSetsStep = createStep(
|
|||||||
const priceSetIds: string[] = []
|
const priceSetIds: string[] = []
|
||||||
|
|
||||||
variantPriceSets.forEach((v) => {
|
variantPriceSets.forEach((v) => {
|
||||||
if (v.price?.price_set_id) {
|
if (v.price_set?.id) {
|
||||||
priceSetIds.push(v.price.price_set_id)
|
priceSetIds.push(v.price_set.id)
|
||||||
} else {
|
} else {
|
||||||
notFound.push(v.id)
|
notFound.push(v.id)
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,8 @@ export const getVariantPriceSetsStep = createStep(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const variantToCalculatedPriceSets = variantPriceSets.reduce(
|
const variantToCalculatedPriceSets = variantPriceSets.reduce(
|
||||||
(acc, { id, price }) => {
|
(acc, { id, price_set }) => {
|
||||||
const calculatedPriceSet = idToPriceSet.get(price?.price_set_id)
|
const calculatedPriceSet = idToPriceSet.get(price_set?.id)
|
||||||
if (calculatedPriceSet) {
|
if (calculatedPriceSet) {
|
||||||
acc[id] = calculatedPriceSet
|
acc[id] = calculatedPriceSet
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { CreatePriceSetDTO, IPricingModuleService } from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export const createPriceSetsStepId = "create-price-sets"
|
||||||
|
export const createPriceSetsStep = createStep(
|
||||||
|
createPriceSetsStepId,
|
||||||
|
async (data: CreatePriceSetDTO[], { container }) => {
|
||||||
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
|
ModuleRegistrationName.PRICING
|
||||||
|
)
|
||||||
|
|
||||||
|
const priceSets = await pricingModule.create(data)
|
||||||
|
|
||||||
|
return new StepResponse(
|
||||||
|
priceSets,
|
||||||
|
priceSets.map((priceSet) => priceSet.id)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async (priceSets, { container }) => {
|
||||||
|
if (!priceSets?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
|
ModuleRegistrationName.PRICING
|
||||||
|
)
|
||||||
|
|
||||||
|
await pricingModule.delete(priceSets)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from "./create-price-sets"
|
||||||
|
export * from "./update-price-sets"
|
||||||
export * from "./create-pricing-rule-types"
|
export * from "./create-pricing-rule-types"
|
||||||
export * from "./delete-pricing-rule-types"
|
export * from "./delete-pricing-rule-types"
|
||||||
export * from "./update-pricing-rule-types"
|
export * from "./update-pricing-rule-types"
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { IPricingModuleService, UpdatePriceSetDTO } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
convertItemResponseToUpdateRequest,
|
||||||
|
getSelectsAndRelationsFromObjectArray,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export const updatePriceSetsStepId = "update-price-sets"
|
||||||
|
export const updatePriceSetsStep = createStep(
|
||||||
|
updatePriceSetsStepId,
|
||||||
|
async (data: UpdatePriceSetDTO[], { container }) => {
|
||||||
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
|
ModuleRegistrationName.PRICING
|
||||||
|
)
|
||||||
|
|
||||||
|
const { selects, relations } = getSelectsAndRelationsFromObjectArray(data)
|
||||||
|
const dataBeforeUpdate = await pricingModule.list(
|
||||||
|
{ id: data.map((d) => d.id) },
|
||||||
|
{ relations, select: selects }
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedPriceSets = await pricingModule.update(data)
|
||||||
|
|
||||||
|
return new StepResponse(updatedPriceSets, {
|
||||||
|
dataBeforeUpdate,
|
||||||
|
selects,
|
||||||
|
relations,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async (revertInput, { container }) => {
|
||||||
|
if (!revertInput) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dataBeforeUpdate = [], selects, relations } = revertInput
|
||||||
|
|
||||||
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
|
ModuleRegistrationName.PRICING
|
||||||
|
)
|
||||||
|
|
||||||
|
await pricingModule.update(
|
||||||
|
dataBeforeUpdate.map((data) =>
|
||||||
|
convertItemResponseToUpdateRequest(data, selects, relations)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -9,7 +9,6 @@ export const createProductVariantsStep = createStep(
|
|||||||
const service = container.resolve<IProductModuleService>(
|
const service = container.resolve<IProductModuleService>(
|
||||||
ModuleRegistrationName.PRODUCT
|
ModuleRegistrationName.PRODUCT
|
||||||
)
|
)
|
||||||
|
|
||||||
const created = await service.createVariants(data)
|
const created = await service.createVariants(data)
|
||||||
return new StepResponse(
|
return new StepResponse(
|
||||||
created,
|
created,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
type StepInput = {
|
||||||
|
links: {
|
||||||
|
variant_id: string
|
||||||
|
price_set_id: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createVariantPricingLinkStepId = "create-variant-pricing-link"
|
||||||
|
export const createVariantPricingLinkStep = createStep(
|
||||||
|
createVariantPricingLinkStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
|
await remoteLink.create(
|
||||||
|
data.links.map((entry) => ({
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: entry.variant_id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: entry.price_set_id,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return new StepResponse(void 0, data)
|
||||||
|
},
|
||||||
|
async (data, { container }) => {
|
||||||
|
if (!data?.links?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
|
const links = data.links.map((entry) => ({
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: entry.variant_id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: entry.price_set_id,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
await remoteLink.dismiss(links)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { IProductModuleService } from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
type StepInput = {
|
||||||
|
ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProductsStepId = "get-products"
|
||||||
|
export const getProductsStep = createStep(
|
||||||
|
getProductsStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
const service = container.resolve<IProductModuleService>(
|
||||||
|
ModuleRegistrationName.PRODUCT
|
||||||
|
)
|
||||||
|
|
||||||
|
const products = await service.list(
|
||||||
|
{ id: data.ids },
|
||||||
|
{ relations: ["variants"], take: null }
|
||||||
|
)
|
||||||
|
return new StepResponse(products, products)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
export * from "./create-products"
|
export * from "./create-products"
|
||||||
export * from "./update-products"
|
export * from "./update-products"
|
||||||
export * from "./delete-products"
|
export * from "./delete-products"
|
||||||
|
export * from "./get-products"
|
||||||
|
export * from "./create-variant-pricing-link"
|
||||||
|
export * from "./remove-variant-pricing-link"
|
||||||
export * from "./create-product-options"
|
export * from "./create-product-options"
|
||||||
export * from "./update-product-options"
|
export * from "./update-product-options"
|
||||||
export * from "./delete-product-options"
|
export * from "./delete-product-options"
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { ILinkModule } from "@medusajs/types"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
type StepInput = {
|
||||||
|
variant_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeVariantPricingLinkStepId = "remove-variant-pricing-link"
|
||||||
|
export const removeVariantPricingLinkStep = createStep(
|
||||||
|
removeVariantPricingLinkStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
|
const linkModule: ILinkModule = remoteLink.getLinkModule(
|
||||||
|
Modules.PRODUCT,
|
||||||
|
"variant_id",
|
||||||
|
Modules.PRICING,
|
||||||
|
"price_set_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
const links = (await linkModule.list(
|
||||||
|
{
|
||||||
|
variant_id: data.variant_ids,
|
||||||
|
},
|
||||||
|
{ select: ["id", "variant_id", "price_set_id"] }
|
||||||
|
)) as { id: string; variant_id: string; price_set_id: string }[]
|
||||||
|
|
||||||
|
await remoteLink.delete(links.map((link) => link.id))
|
||||||
|
return new StepResponse(void 0, links)
|
||||||
|
},
|
||||||
|
async (prevData, { container }) => {
|
||||||
|
if (!prevData?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
|
await remoteLink.create(
|
||||||
|
prevData.map((entry) => ({
|
||||||
|
[Modules.PRODUCT]: {
|
||||||
|
variant_id: entry.variant_id,
|
||||||
|
},
|
||||||
|
[Modules.PRICING]: {
|
||||||
|
price_set_id: entry.price_set_id,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
import { ProductTypes } from "@medusajs/types"
|
import { ProductTypes, PricingTypes } from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import {
|
||||||
import { createProductVariantsStep } from "../steps"
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
createProductVariantsStep,
|
||||||
|
createVariantPricingLinkStep,
|
||||||
|
} from "../steps"
|
||||||
|
import { createPriceSetsStep } from "../../pricing"
|
||||||
|
|
||||||
|
// TODO: Create separate typings for the workflow input
|
||||||
type WorkflowInput = {
|
type WorkflowInput = {
|
||||||
product_variants: ProductTypes.CreateProductVariantDTO[]
|
product_variants: (ProductTypes.CreateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createProductVariantsWorkflowId = "create-product-variants"
|
export const createProductVariantsWorkflowId = "create-product-variants"
|
||||||
@@ -12,6 +23,71 @@ export const createProductVariantsWorkflow = createWorkflow(
|
|||||||
(
|
(
|
||||||
input: WorkflowData<WorkflowInput>
|
input: WorkflowData<WorkflowInput>
|
||||||
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
||||||
return createProductVariantsStep(input.product_variants)
|
// Passing prices to the product module will fail, we want to keep them for after the variant is created.
|
||||||
|
const variantsWithoutPrices = transform({ input }, (data) =>
|
||||||
|
data.input.product_variants.map((v) => ({
|
||||||
|
...v,
|
||||||
|
prices: undefined,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const createdVariants = createProductVariantsStep(variantsWithoutPrices)
|
||||||
|
|
||||||
|
// Note: We rely on the same order of input and output when creating variants here, make sure that assumption holds
|
||||||
|
const variantsWithAssociatedPrices = transform(
|
||||||
|
{ input, createdVariants },
|
||||||
|
(data) =>
|
||||||
|
data.createdVariants
|
||||||
|
.map((variant, i) => {
|
||||||
|
return {
|
||||||
|
id: variant.id,
|
||||||
|
prices: data.input.product_variants[i]?.prices,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
.filter((v) => !!v.prices?.length)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: From here until the final transform the code is the same as when creating a product, we can probably refactor
|
||||||
|
const createdPriceSets = createPriceSetsStep(variantsWithAssociatedPrices)
|
||||||
|
|
||||||
|
const variantAndPriceSets = transform(
|
||||||
|
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||||
|
(data) => {
|
||||||
|
return data.variantsWithAssociatedPrices.map((variant, i) => ({
|
||||||
|
variant: variant,
|
||||||
|
price_set: data.createdPriceSets[i],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantAndPriceSetLinks = transform(
|
||||||
|
{ variantAndPriceSets },
|
||||||
|
(data) => {
|
||||||
|
return {
|
||||||
|
links: data.variantAndPriceSets.map((entry) => ({
|
||||||
|
variant_id: entry.variant.id,
|
||||||
|
price_set_id: entry.price_set.id,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
createVariantPricingLinkStep(variantAndPriceSetLinks)
|
||||||
|
|
||||||
|
return transform(
|
||||||
|
{
|
||||||
|
createdVariants,
|
||||||
|
variantAndPriceSets,
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
return data.createdVariants.map((variant) => ({
|
||||||
|
...variant,
|
||||||
|
price_set: data.variantAndPriceSets.find(
|
||||||
|
(v) => v.variant.id === variant.id
|
||||||
|
)?.price_set,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,21 @@
|
|||||||
import { ProductTypes } from "@medusajs/types"
|
import { ProductTypes, PricingTypes } from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import {
|
||||||
import { createProductsStep } from "../steps"
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { createProductsStep, createVariantPricingLinkStep } from "../steps"
|
||||||
|
import { createPriceSetsStep } from "../../pricing"
|
||||||
|
|
||||||
type WorkflowInput = { products: ProductTypes.CreateProductDTO[] }
|
// TODO: We should have separate types here as input, not the module DTO. Eg. the HTTP request that we are handling
|
||||||
|
// has different data than the DTO, so that needs to be represented differently.
|
||||||
|
type WorkflowInput = {
|
||||||
|
products: (Omit<ProductTypes.CreateProductDTO, "variants"> & {
|
||||||
|
variants?: (ProductTypes.CreateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
})[]
|
||||||
|
}
|
||||||
|
|
||||||
export const createProductsWorkflowId = "create-products"
|
export const createProductsWorkflowId = "create-products"
|
||||||
export const createProductsWorkflow = createWorkflow(
|
export const createProductsWorkflow = createWorkflow(
|
||||||
@@ -10,6 +23,78 @@ export const createProductsWorkflow = createWorkflow(
|
|||||||
(
|
(
|
||||||
input: WorkflowData<WorkflowInput>
|
input: WorkflowData<WorkflowInput>
|
||||||
): WorkflowData<ProductTypes.ProductDTO[]> => {
|
): WorkflowData<ProductTypes.ProductDTO[]> => {
|
||||||
return createProductsStep(input.products)
|
// Passing prices to the product module will fail, we want to keep them for after the product is created.
|
||||||
|
const productWithoutPrices = transform({ input }, (data) =>
|
||||||
|
data.input.products.map((p) => ({
|
||||||
|
...p,
|
||||||
|
variants: p.variants?.map((v) => ({
|
||||||
|
...v,
|
||||||
|
prices: undefined,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const createdProducts = createProductsStep(productWithoutPrices)
|
||||||
|
|
||||||
|
// Note: We rely on the same order of input and output when creating products here, make sure that assumption holds
|
||||||
|
const variantsWithAssociatedPrices = transform(
|
||||||
|
{ input, createdProducts },
|
||||||
|
(data) => {
|
||||||
|
return data.createdProducts
|
||||||
|
.map((p, i) => {
|
||||||
|
const inputProduct = data.input.products[i]
|
||||||
|
return p.variants?.map((v, j) => ({
|
||||||
|
id: v.id,
|
||||||
|
prices: inputProduct?.variants?.[j]?.prices,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
.filter((v) => !!v.prices?.length)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const createdPriceSets = createPriceSetsStep(variantsWithAssociatedPrices)
|
||||||
|
|
||||||
|
const variantAndPriceSets = transform(
|
||||||
|
{ variantsWithAssociatedPrices, createdPriceSets },
|
||||||
|
(data) =>
|
||||||
|
data.variantsWithAssociatedPrices.map((variant, i) => ({
|
||||||
|
variant: variant,
|
||||||
|
price_set: data.createdPriceSets[i],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantAndPriceSetLinks = transform(
|
||||||
|
{ variantAndPriceSets },
|
||||||
|
(data) => {
|
||||||
|
return {
|
||||||
|
links: data.variantAndPriceSets.map((entry) => ({
|
||||||
|
variant_id: entry.variant.id,
|
||||||
|
price_set_id: entry.price_set.id,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
createVariantPricingLinkStep(variantAndPriceSetLinks)
|
||||||
|
|
||||||
|
// TODO: Should we just refetch the products here?
|
||||||
|
return transform(
|
||||||
|
{
|
||||||
|
createdProducts,
|
||||||
|
variantAndPriceSets,
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
return data.createdProducts.map((product) => ({
|
||||||
|
...product,
|
||||||
|
variants: product.variants?.map((variant) => ({
|
||||||
|
...variant,
|
||||||
|
price_set: data.variantAndPriceSets.find(
|
||||||
|
(v) => v.variant.id === variant.id
|
||||||
|
)?.price_set,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
import { deleteProductVariantsStep } from "../steps"
|
import {
|
||||||
|
deleteProductVariantsStep,
|
||||||
|
removeVariantPricingLinkStep,
|
||||||
|
} from "../steps"
|
||||||
|
|
||||||
type WorkflowInput = { ids: string[] }
|
type WorkflowInput = { ids: string[] }
|
||||||
|
|
||||||
@@ -7,6 +10,8 @@ export const deleteProductVariantsWorkflowId = "delete-product-variants"
|
|||||||
export const deleteProductVariantsWorkflow = createWorkflow(
|
export const deleteProductVariantsWorkflow = createWorkflow(
|
||||||
deleteProductVariantsWorkflowId,
|
deleteProductVariantsWorkflowId,
|
||||||
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
||||||
|
// Question: Should we also remove the price set manually, or would that be cascaded?
|
||||||
|
removeVariantPricingLinkStep({ variant_ids: input.ids })
|
||||||
return deleteProductVariantsStep(input.ids)
|
return deleteProductVariantsStep(input.ids)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import {
|
||||||
import { deleteProductsStep } from "../steps"
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
deleteProductsStep,
|
||||||
|
getProductsStep,
|
||||||
|
removeVariantPricingLinkStep,
|
||||||
|
} from "../steps"
|
||||||
|
|
||||||
type WorkflowInput = { ids: string[] }
|
type WorkflowInput = { ids: string[] }
|
||||||
|
|
||||||
@@ -7,6 +15,17 @@ export const deleteProductsWorkflowId = "delete-products"
|
|||||||
export const deleteProductsWorkflow = createWorkflow(
|
export const deleteProductsWorkflow = createWorkflow(
|
||||||
deleteProductsWorkflowId,
|
deleteProductsWorkflowId,
|
||||||
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
||||||
|
const productsToDelete = getProductsStep({ ids: input.ids })
|
||||||
|
const variantsToBeDeleted = transform({ productsToDelete }, (data) => {
|
||||||
|
return data.productsToDelete
|
||||||
|
.flatMap((product) => product.variants)
|
||||||
|
.map((variant) => variant.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Question: Should we also remove the price set manually, or would that be cascaded?
|
||||||
|
// Question: Since we soft-delete the product, how do we restore the product with the prices and the links?
|
||||||
|
removeVariantPricingLinkStep({ variant_ids: variantsToBeDeleted })
|
||||||
|
|
||||||
return deleteProductsStep(input.ids)
|
return deleteProductsStep(input.ids)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -41,11 +41,14 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = {
|
|||||||
extends: [
|
extends: [
|
||||||
{
|
{
|
||||||
serviceName: Modules.PRODUCT,
|
serviceName: Modules.PRODUCT,
|
||||||
|
fieldAlias: {
|
||||||
|
price_set: "price_set_link.price_set",
|
||||||
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
serviceName: LINKS.ProductVariantPriceSet,
|
serviceName: LINKS.ProductVariantPriceSet,
|
||||||
primaryKey: "variant_id",
|
primaryKey: "variant_id",
|
||||||
foreignKey: "id",
|
foreignKey: "id",
|
||||||
alias: "price",
|
alias: "price_set_link",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const GET = async (
|
|||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "product_option",
|
entryPoint: "product_option",
|
||||||
variables,
|
variables,
|
||||||
fields: req.retrieveConfig.select as string[],
|
fields: req.remoteQueryConfig.fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [product_option] = await remoteQuery(queryObject)
|
const [product_option] = await remoteQuery(queryObject)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const GET = async (
|
|||||||
skip: req.listConfig.skip,
|
skip: req.listConfig.skip,
|
||||||
take: req.listConfig.take,
|
take: req.listConfig.take,
|
||||||
},
|
},
|
||||||
fields: req.listConfig.select as string[],
|
fields: req.remoteQueryConfig.fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { rows: product_options, metadata } = await remoteQuery(queryObject)
|
const { rows: product_options, metadata } = await remoteQuery(queryObject)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
|
|
||||||
import { UpdateProductDTO } from "@medusajs/types"
|
import { UpdateProductDTO } from "@medusajs/types"
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
|
import { remapKeysForProduct, remapProduct } from "../helpers"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
@@ -18,15 +19,16 @@ export const GET = async (
|
|||||||
|
|
||||||
const variables = { id: req.params.id }
|
const variables = { id: req.params.id }
|
||||||
|
|
||||||
|
const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? [])
|
||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "product",
|
entryPoint: "product",
|
||||||
variables,
|
variables,
|
||||||
fields: req.retrieveConfig.select as string[],
|
fields: selectFields,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [product] = await remoteQuery(queryObject)
|
const [product] = await remoteQuery(queryObject)
|
||||||
|
|
||||||
res.status(200).json({ product })
|
res.status(200).json({ product: remapProduct(product) })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
@@ -45,7 +47,7 @@ export const POST = async (
|
|||||||
throw errors[0].error
|
throw errors[0].error
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ product: result[0] })
|
res.status(200).json({ product: remapProduct(result[0]) })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DELETE = async (
|
export const DELETE = async (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { UpdateProductVariantDTO } from "@medusajs/types"
|
import { UpdateProductVariantDTO } from "@medusajs/types"
|
||||||
import { defaultAdminProductsVariantFields } from "../../../query-config"
|
import { defaultAdminProductsVariantFields } from "../../../query-config"
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
|
import { remapKeysForVariant, remapVariant } from "../../../helpers"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
@@ -26,11 +27,11 @@ export const GET = async (
|
|||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "variant",
|
entryPoint: "variant",
|
||||||
variables,
|
variables,
|
||||||
fields: req.retrieveConfig.select as string[],
|
fields: remapKeysForVariant(req.remoteQueryConfig.fields ?? []),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [variant] = await remoteQuery(queryObject)
|
const [variant] = await remoteQuery(queryObject)
|
||||||
res.status(200).json({ variant })
|
res.status(200).json({ variant: remapVariant(variant) })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
@@ -55,7 +56,7 @@ export const POST = async (
|
|||||||
throw errors[0].error
|
throw errors[0].error
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ variant: result[0] })
|
res.status(200).json({ variant: remapVariant(result[0]) })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DELETE = async (
|
export const DELETE = async (
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import {
|
|||||||
import { CreateProductVariantDTO } from "@medusajs/types"
|
import { CreateProductVariantDTO } from "@medusajs/types"
|
||||||
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
|
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
remapKeysForProduct,
|
||||||
|
remapKeysForVariant,
|
||||||
|
remapProduct,
|
||||||
|
remapVariant,
|
||||||
|
} from "../../helpers"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
@@ -22,13 +28,13 @@ export const GET = async (
|
|||||||
skip: req.listConfig.skip,
|
skip: req.listConfig.skip,
|
||||||
take: req.listConfig.take,
|
take: req.listConfig.take,
|
||||||
},
|
},
|
||||||
fields: req.listConfig.select as string[],
|
fields: remapKeysForVariant(req.remoteQueryConfig.fields ?? []),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { rows: variants, metadata } = await remoteQuery(queryObject)
|
const { rows: variants, metadata } = await remoteQuery(queryObject)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
variants,
|
variants: variants.map(remapVariant),
|
||||||
count: metadata.count,
|
count: metadata.count,
|
||||||
offset: metadata.skip,
|
offset: metadata.skip,
|
||||||
limit: metadata.take,
|
limit: metadata.take,
|
||||||
@@ -58,5 +64,15 @@ export const POST = async (
|
|||||||
throw errors[0].error
|
throw errors[0].error
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ variant: result[0] })
|
const remoteQuery = req.scope.resolve("remoteQuery")
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "product",
|
||||||
|
variables: {
|
||||||
|
filters: { id: productId },
|
||||||
|
},
|
||||||
|
fields: remapKeysForProduct(req.remoteQueryConfig.fields ?? []),
|
||||||
|
})
|
||||||
|
|
||||||
|
const products = await remoteQuery(queryObject)
|
||||||
|
res.status(200).json({ product: remapProduct(products[0]) })
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ProductDTO, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
// The variant had prices before, but that is not part of the price_set money amounts. Do we remap the request and response or not?
|
||||||
|
export const remapKeysForProduct = (selectFields: string[]) => {
|
||||||
|
const productFields = selectFields.filter(
|
||||||
|
(fieldName: string) => !fieldName.startsWith("variants.prices")
|
||||||
|
)
|
||||||
|
const pricingFields = selectFields
|
||||||
|
.filter((fieldName: string) => fieldName.startsWith("variants.prices"))
|
||||||
|
.map((fieldName: string) =>
|
||||||
|
fieldName.replace("variants.prices.", "variants.price_set.money_amounts.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return [...productFields, ...pricingFields]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remapKeysForVariant = (selectFields: string[]) => {
|
||||||
|
const variantFields = selectFields.filter(
|
||||||
|
(fieldName: string) => !fieldName.startsWith("prices")
|
||||||
|
)
|
||||||
|
const pricingFields = selectFields
|
||||||
|
.filter((fieldName: string) => fieldName.startsWith("prices"))
|
||||||
|
.map((fieldName: string) =>
|
||||||
|
fieldName.replace("prices.", "price_set.money_amounts.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return [...variantFields, ...pricingFields]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remapProduct = (p: ProductDTO) => {
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
variants: p.variants?.map(remapVariant),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remapVariant = (v: ProductVariantDTO) => {
|
||||||
|
return {
|
||||||
|
...v,
|
||||||
|
prices: (v as any).price_set?.money_amounts?.map((ma) => ({
|
||||||
|
...ma,
|
||||||
|
variant_id: v.id,
|
||||||
|
})),
|
||||||
|
price_set: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,14 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
matcher: "/admin/products/:id/variants",
|
matcher: "/admin/products/:id/variants",
|
||||||
middlewares: [transformBody(AdminPostProductsProductVariantsReq)],
|
middlewares: [
|
||||||
|
transformBody(AdminPostProductsProductVariantsReq),
|
||||||
|
// We specify the product here as that's what we return after updating the variant
|
||||||
|
transformQuery(
|
||||||
|
AdminGetProductsProductParams,
|
||||||
|
QueryConfig.retrieveTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export const defaultAdminProductsVariantFields = [
|
|||||||
"ean",
|
"ean",
|
||||||
"upc",
|
"upc",
|
||||||
"barcode",
|
"barcode",
|
||||||
|
"prices.id",
|
||||||
|
"prices.currency_code",
|
||||||
|
"prices.amount",
|
||||||
|
"prices.created_at",
|
||||||
|
"prices.updated_at",
|
||||||
"options.id",
|
"options.id",
|
||||||
"options.option_value.value",
|
"options.option_value.value",
|
||||||
"options.option_value.option.title",
|
"options.option_value.option.title",
|
||||||
@@ -55,7 +60,6 @@ export const listOptionConfig = {
|
|||||||
|
|
||||||
/* export const allowedAdminProductRelations = [
|
/* export const allowedAdminProductRelations = [
|
||||||
"variants",
|
"variants",
|
||||||
// TODO: Add in next iteration
|
|
||||||
// "variants.prices",
|
// "variants.prices",
|
||||||
"variants.options",
|
"variants.options",
|
||||||
"images",
|
"images",
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import {
|
|||||||
} from "../../../types/routing"
|
} from "../../../types/routing"
|
||||||
import { listPriceLists } from "../price-lists/queries"
|
import { listPriceLists } from "../price-lists/queries"
|
||||||
import { AdminGetProductsParams } from "./validators"
|
import { AdminGetProductsParams } from "./validators"
|
||||||
|
import { remapKeysForProduct, remapProduct } from "./helpers"
|
||||||
|
import { MedusaContainer } from "medusa-core-utils"
|
||||||
|
|
||||||
export const GET = async (
|
const applyVariantFiltersForPriceList = async (
|
||||||
req: AuthenticatedMedusaRequest<AdminGetProductsParams>,
|
scope: MedusaContainer,
|
||||||
res: MedusaResponse
|
filterableFields: AdminGetProductsParams
|
||||||
) => {
|
) => {
|
||||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
|
||||||
const filterableFields: AdminGetProductsParams = { ...req.filterableFields }
|
|
||||||
const filterByPriceListIds = filterableFields.price_list_id
|
const filterByPriceListIds = filterableFields.price_list_id
|
||||||
const priceListVariantIds: string[] = []
|
const priceListVariantIds: string[] = []
|
||||||
|
|
||||||
@@ -25,17 +25,17 @@ export const GET = async (
|
|||||||
// the variant IDs through the price list price sets.
|
// the variant IDs through the price list price sets.
|
||||||
if (Array.isArray(filterByPriceListIds)) {
|
if (Array.isArray(filterByPriceListIds)) {
|
||||||
const [priceLists] = await listPriceLists({
|
const [priceLists] = await listPriceLists({
|
||||||
container: req.scope,
|
container: scope,
|
||||||
remoteQueryFields: ["price_set_money_amounts.price_set.variant.id"],
|
remoteQueryFields: ["price_set_money_amounts.price_set.variant.id"],
|
||||||
apiFields: ["prices.variant_id"],
|
apiFields: ["prices.variant_id"],
|
||||||
variables: { filters: { id: filterByPriceListIds }, skip: 0, take: null },
|
variables: { filters: { id: filterByPriceListIds }, skip: 0, take: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
priceListVariantIds.push(
|
priceListVariantIds.push(
|
||||||
...(priceLists
|
...((priceLists
|
||||||
.map((priceList) => priceList.prices?.map((price) => price.variant_id))
|
.map((priceList) => priceList.prices?.map((price) => price.variant_id))
|
||||||
.flat(2)
|
.flat(2)
|
||||||
.filter(isString) || [])
|
.filter(isString) || []) as string[])
|
||||||
)
|
)
|
||||||
|
|
||||||
delete filterableFields.price_list_id
|
delete filterableFields.price_list_id
|
||||||
@@ -50,19 +50,34 @@ export const GET = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filterableFields
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminGetProductsParams>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
let filterableFields: AdminGetProductsParams = { ...req.filterableFields }
|
||||||
|
filterableFields = await applyVariantFiltersForPriceList(
|
||||||
|
req.scope,
|
||||||
|
filterableFields
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? [])
|
||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "product",
|
entryPoint: "product",
|
||||||
variables: {
|
variables: {
|
||||||
filters: filterableFields,
|
filters: filterableFields,
|
||||||
...req.remoteQueryConfig.pagination,
|
...req.remoteQueryConfig.pagination,
|
||||||
},
|
},
|
||||||
fields: req.remoteQueryConfig.fields,
|
fields: selectFields,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { rows: products, metadata } = await remoteQuery(queryObject)
|
const { rows: products, metadata } = await remoteQuery(queryObject)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
products,
|
products: products.map(remapProduct),
|
||||||
count: metadata.count,
|
count: metadata.count,
|
||||||
offset: metadata.skip,
|
offset: metadata.skip,
|
||||||
limit: metadata.take,
|
limit: metadata.take,
|
||||||
@@ -88,5 +103,5 @@ export const POST = async (
|
|||||||
throw errors[0].error
|
throw errors[0].error
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ product: result[0] })
|
res.status(200).json({ product: remapProduct(result[0]) })
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,13 @@ import {
|
|||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
NotEquals,
|
NotEquals,
|
||||||
|
Validate,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
@@ -17,6 +19,7 @@ import { FindParams, extendedFindParamsMixin } from "../../../types/common"
|
|||||||
import { OperatorMapValidator } from "../../../types/validators/operator-map"
|
import { OperatorMapValidator } from "../../../types/validators/operator-map"
|
||||||
import { IsType } from "../../../utils"
|
import { IsType } from "../../../utils"
|
||||||
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
import { optionalBooleanMapper } from "../../../utils/validators/is-boolean"
|
||||||
|
import { XorConstraint } from "../../../types/validators/xor"
|
||||||
|
|
||||||
export class AdminGetProductsProductParams extends FindParams {}
|
export class AdminGetProductsProductParams extends FindParams {}
|
||||||
export class AdminGetProductsProductVariantsVariantParams extends FindParams {}
|
export class AdminGetProductsProductVariantsVariantParams extends FindParams {}
|
||||||
@@ -537,13 +540,10 @@ export class AdminPostProductsProductVariantsReq {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|
||||||
// TODO: Add on next iteration, adding temporary field for now
|
|
||||||
// @IsArray()
|
|
||||||
// @ValidateNested({ each: true })
|
|
||||||
// @Type(() => ProductVariantPricesCreateReq)
|
|
||||||
// prices: ProductVariantPricesCreateReq[]
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
prices: any[]
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ProductVariantPricesCreateReq)
|
||||||
|
prices: ProductVariantPricesCreateReq[]
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@@ -619,12 +619,11 @@ export class AdminPostProductsProductVariantsVariantReq {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|
||||||
// TODO: Deal with in next iteration
|
@IsArray()
|
||||||
// @IsArray()
|
@IsOptional()
|
||||||
// @IsOptional()
|
@ValidateNested({ each: true })
|
||||||
// @ValidateNested({ each: true })
|
@Type(() => ProductVariantPricesUpdateReq)
|
||||||
// @Type(() => ProductVariantPricesUpdateReq)
|
prices?: ProductVariantPricesUpdateReq[]
|
||||||
// prices?: ProductVariantPricesUpdateReq[]
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@@ -679,3 +678,41 @@ export class ProductTypeReq {
|
|||||||
@IsString()
|
@IsString()
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add support for rules
|
||||||
|
export class ProductVariantPricesCreateReq {
|
||||||
|
@IsString()
|
||||||
|
currency_code: string
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
amount: number
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
min_quantity?: number
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
max_quantity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProductVariantPricesUpdateReq {
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
id?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
currency_code?: string
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
amount: number
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
min_quantity?: number
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
max_quantity?: number
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseFilterable } from "../../dal";
|
import { BaseFilterable } from "../../dal"
|
||||||
import { CreatePriceSetPriceRules } from "./price-list";
|
import { CreatePriceSetPriceRules } from "./price-list"
|
||||||
import {
|
import {
|
||||||
CreateMoneyAmountDTO,
|
CreateMoneyAmountDTO,
|
||||||
FilterableMoneyAmountProps,
|
FilterableMoneyAmountProps,
|
||||||
|
|||||||
Reference in new issue
Block a user