diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 3037f63294..f6f63f3c70 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -27,9 +27,52 @@ let { 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({ env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }, testSuite: ({ dbConnection, getContainer, api }) => { + let v2Product beforeAll(() => { // Note: We have to lazily load everything because there are weird ordering issues when doing `require` of `@medusajs/medusa` productSeeder = require("../../../helpers/product-seeder") @@ -55,6 +98,15 @@ medusaIntegrationTestRunner({ beforeEach(async () => { const container = getContainer() 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", () => { @@ -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 () => { await simplePriceListFactory(dbConnection, { prices: [ @@ -305,8 +357,7 @@ medusaIntegrationTestRunner({ ) }) - // TODO: Reenable once `tags.*` and `+` and `-` operators are supported - it.skip("doesn't expand collection and types", async () => { + it("doesn't expand collection and types", async () => { const notExpected = [ expect.objectContaining({ collection: expect.any(Object), @@ -316,7 +367,10 @@ medusaIntegrationTestRunner({ const response = await api .get( - `/admin/products?status[]=published,proposed&expand=tags`, + `/admin/products?status[]=published,proposed&${breaking( + () => "expand=tags", + () => "fields=id,status,*tags" + )}`, adminHeaders ) .catch((err) => { @@ -381,10 +435,15 @@ medusaIntegrationTestRunner({ expect(response.data.products.length).toEqual(2) }) - // TODO: Enable once pricing is available - it.skip("returns a list of products with free text query including variant prices", async () => { + it("returns a list of products with free text query including variant prices", async () => { const response = await api - .get("/admin/products?q=test+product1", adminHeaders) + .get( + `/admin/products?q=${breaking( + () => "test+product1", + () => v2Product.description + )}`, + adminHeaders + ) .catch((err) => { console.log(err) }) @@ -397,10 +456,16 @@ medusaIntegrationTestRunner({ expect(expectedVariantPrices).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-price_4", + id: breaking( + () => "test-price_4", + () => expect.stringMatching(/^ma_*/) + ), }), 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.data.products.length).toEqual(4) + expect(response.data.products.length).toEqual( + breaking( + () => 4, + () => 5 + ) + ) }) it("returns a list of deleted products", async () => { @@ -639,16 +709,16 @@ medusaIntegrationTestRunner({ product_id: expect.stringMatching(/^prod_*/), created_at: expect.any(String), updated_at: expect.any(String), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: expect.any(String), - // currency_code: "usd", - // amount: 100, - // variant_id: expect.stringMatching(/^variant_*/), - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + currency_code: "usd", + amount: 100, + variant_id: expect.stringMatching(/^variant_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -719,7 +789,6 @@ medusaIntegrationTestRunner({ console.log(err) }) - console.log(JSON.stringify(response.data.products, null, 2)) // TODO: Enable other assertions once supported expect(response.data.products).toHaveLength(5) expect(response.data.products).toEqual( @@ -747,14 +816,14 @@ medusaIntegrationTestRunner({ created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: "test-price", - // variant_id: expect.stringMatching(/^test-variant*/), - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: "test-price", + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -782,14 +851,14 @@ medusaIntegrationTestRunner({ created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: expect.stringMatching(/^test-price*/), - // variant_id: "test-variant_2", - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: "test-variant_2", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -817,14 +886,14 @@ medusaIntegrationTestRunner({ created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: expect.stringMatching(/^test-price*/), - // variant_id: expect.stringMatching(/^test-variant*/), - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -852,14 +921,14 @@ medusaIntegrationTestRunner({ created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: "test-price-sale", - // variant_id: expect.stringMatching(/^test-variant*/), - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: "test-price-sale", + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -914,14 +983,14 @@ medusaIntegrationTestRunner({ created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: expect.stringMatching(/^test-price*/), - // variant_id: expect.stringMatching(/^test-variant*/), - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -949,14 +1018,14 @@ medusaIntegrationTestRunner({ created_at: expect.any(String), updated_at: expect.any(String), product_id: expect.stringMatching(/^test-*/), - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: expect.stringMatching(/^test-price*/), - // variant_id: expect.stringMatching(/^test-variant*/), - // created_at: expect.any(String), - // updated_at: expect.any(String), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), options: breaking( () => expect.arrayContaining([ @@ -1047,20 +1116,21 @@ medusaIntegrationTestRunner({ variants: [ { title: "Test variant", - // prices: [ - // { - // currency: "usd", - // amount: 100, - // }, - // ], + prices: [ + { + currency: "usd", + amount: 100, + }, + ], }, ], }) }) it("should get a product with default relations", async () => { + const testProductId = v2Product?.id ?? productId const res = await api - .get(`/admin/products/${productId}`, adminHeaders) + .get(`/admin/products/${testProductId}`, adminHeaders) .catch((err) => { console.log(err) }) @@ -1068,7 +1138,7 @@ medusaIntegrationTestRunner({ const keysInResponse = Object.keys(res.data.product) expect(res.status).toEqual(200) - expect(res.data.product.id).toEqual(productId) + expect(res.data.product.id).toEqual(testProductId) expect(keysInResponse).toEqual( expect.arrayContaining([ "id", @@ -1112,17 +1182,20 @@ medusaIntegrationTestRunner({ ]) ) - // const variants = res.data.product.variants - // const hasPrices = variants.some((variant) => !!variant.prices) + const variants = res.data.product.variants + const hasPrices = variants.some((variant) => !!variant.prices) - // expect(hasPrices).toBe(true) + expect(hasPrices).toBe(true) }) - // TODO: Enable once pricing is available - it.skip("should get a product with prices", async () => { + it("should get a product with prices", async () => { + const testProductId = v2Product?.id ?? productId const res = await api .get( - `/admin/products/${productId}?expand=variants,variants.prices`, + `/admin/products/${testProductId}?${breaking( + () => "expand=variants,variants.prices", + () => "fields=*variants,*variants.prices" + )}`, adminHeaders ) .catch((err) => { @@ -1131,7 +1204,7 @@ medusaIntegrationTestRunner({ const { id, variants } = res.data.product - expect(id).toEqual(productId) + expect(id).toEqual(testProductId) expect(variants[0].prices).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -1142,17 +1215,23 @@ medusaIntegrationTestRunner({ ) }) - // TODO: Reenable once `variants.*` and `+` and `-` operators are supported - it.skip("should get a product only with variants expanded", async () => { + it("should get a product only with variants expanded", async () => { + const testProductId = v2Product?.id ?? productId const res = await api - .get(`/admin/products/${productId}?expand=variants`, adminHeaders) + .get( + `/admin/products/${testProductId}?${breaking( + () => "expand=variants", + () => "fields=title,*variants" + )}`, + adminHeaders + ) .catch((err) => { console.log(err) }) const { id, variants } = res.data.product - expect(id).toEqual(productId) + expect(id).toEqual(testProductId) expect(variants[0]).toEqual( expect.objectContaining({ title: "Test variant", @@ -1175,51 +1254,12 @@ medusaIntegrationTestRunner({ }) 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 - .post("/admin/products", payload, adminHeaders) + .post( + "/admin/products", + { ...productFixture, title: "Test create" }, + adminHeaders + ) .catch((err) => { console.log(err) }) @@ -1229,10 +1269,10 @@ medusaIntegrationTestRunner({ expect(response.data.product).toEqual( expect.objectContaining({ id: expect.stringMatching(/^prod_*/), - title: "Test", + title: "Test create", discountable: true, is_giftcard: false, - handle: "test", + handle: "test-create", status: "draft", created_at: expect.any(String), updated_at: expect.any(String), @@ -1318,32 +1358,32 @@ medusaIntegrationTestRunner({ updated_at: expect.any(String), created_at: expect.any(String), title: "Test variant", - // prices: expect.arrayContaining([ - // expect.objectContaining({ - // id: expect.stringMatching(/^ma_*/), - // currency_code: "usd", - // amount: 100, - // created_at: expect.any(String), - // updated_at: expect.any(String), - // variant_id: expect.stringMatching(/^variant_*/), - // }), - // expect.objectContaining({ - // id: expect.stringMatching(/^ma_*/), - // currency_code: "eur", - // amount: 45, - // created_at: expect.any(String), - // updated_at: expect.any(String), - // variant_id: expect.stringMatching(/^variant_*/), - // }), - // expect.objectContaining({ - // id: expect.stringMatching(/^ma_*/), - // currency_code: "dkk", - // amount: 30, - // created_at: expect.any(String), - // updated_at: expect.any(String), - // variant_id: expect.stringMatching(/^variant_*/), - // }), - // ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^ma_*/), + currency_code: "usd", + amount: 100, + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + }), + expect.objectContaining({ + id: expect.stringMatching(/^ma_*/), + currency_code: "eur", + amount: 45, + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + }), + expect.objectContaining({ + id: expect.stringMatching(/^ma_*/), + currency_code: "dkk", + amount: 30, + created_at: expect.any(String), + updated_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + }), + ]), // TODO: `option_value` not returned on creation. // options: breaking( // () => @@ -1402,13 +1442,11 @@ medusaIntegrationTestRunner({ images: ["test-image.png", "test-image-2.png"], collection_id: "test-collection", tags: [{ value: "123" }, { value: "456" }], - // options: [{ title: "size" }, { title: "color" }], variants: [ { title: "Test variant", inventory_quantity: 10, 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 () => { const payload = { collection_id: null, - // TODO: We try to insert the variants, check - // variants: [ - // { - // id: "test-variant", - // title: "New variant", - // // prices: [ - // // { - // // currency_code: "usd", - // // amount: 75, - // // }, - // // ], - // }, - // ], + variants: [ + { + id: "test-variant", + title: "New variant", + // prices: [ + // { + // currency_code: "usd", + // amount: 75, + // }, + // ], + }, + ], tags: [{ value: "123" }], images: ["test-image-2.png"], 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)", () => { beforeEach(async () => { await productSeeder(dbConnection) @@ -2385,8 +2422,7 @@ medusaIntegrationTestRunner({ }) }) - // TODO: Add once pricing is enabled - describe.skip("variant creation", () => { + describe("variant creation", () => { beforeEach(async () => { try { await productSeeder(dbConnection) @@ -2414,11 +2450,21 @@ medusaIntegrationTestRunner({ amount: 100, }, { - region_id: "test-region", + ...breaking( + () => ({ region_id: "test-region" }), + () => ({ currency_code: "eur" }) + ), amount: 200, }, ], - options: [{ option_id: "test-option", value: "inserted value" }], + ...breaking( + () => ({ + options: [ + { option_id: "test-option", value: "inserted value" }, + ], + }), + () => ({}) + ), } const res = await api @@ -2441,19 +2487,32 @@ medusaIntegrationTestRunner({ expect.objectContaining({ currency_code: "usd", amount: 100, - min_quantity: null, - max_quantity: null, variant_id: insertedVariant.id, - region_id: null, + ...breaking( + () => ({ + region_id: null, + min_quantity: null, + max_quantity: null, + }), + () => ({}) + ), }), expect.objectContaining({ - currency_code: "usd", + currency_code: breaking( + () => "usd", + () => "eur" + ), amount: 200, - min_quantity: null, - max_quantity: null, - price_list_id: null, 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 const optValPre = await dbConnection.manager.findOne( 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 const pricePre = await dbConnection.manager.findOne(MoneyAmount, { where: { id: "test-price" }, @@ -2744,7 +2804,7 @@ medusaIntegrationTestRunner({ 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 const payload = { title: "Test product", @@ -2864,7 +2924,6 @@ medusaIntegrationTestRunner({ amount: 100, }, ], - // options: [{ option_id: "test-option", value: "inserted value" }], } const res = await api @@ -2907,7 +2966,7 @@ medusaIntegrationTestRunner({ .post( "/admin/products/test-product-to-update/variants/test-variant-to-update", { - inventory_quantity: 10, + title: "Updated variant", }, adminHeaders ) diff --git a/packages/core-flows/src/definition/cart/steps/get-variant-price-sets.ts b/packages/core-flows/src/definition/cart/steps/get-variant-price-sets.ts index 58e4a3c9f6..413f09e79f 100644 --- a/packages/core-flows/src/definition/cart/steps/get-variant-price-sets.ts +++ b/packages/core-flows/src/definition/cart/steps/get-variant-price-sets.ts @@ -26,8 +26,8 @@ export const getVariantPriceSetsStep = createStep( { variant: { fields: ["id"], - price: { - fields: ["price_set_id"], + price_set: { + fields: ["id"], }, }, }, @@ -42,8 +42,8 @@ export const getVariantPriceSetsStep = createStep( const priceSetIds: string[] = [] variantPriceSets.forEach((v) => { - if (v.price?.price_set_id) { - priceSetIds.push(v.price.price_set_id) + if (v.price_set?.id) { + priceSetIds.push(v.price_set.id) } else { notFound.push(v.id) } @@ -66,8 +66,8 @@ export const getVariantPriceSetsStep = createStep( ) const variantToCalculatedPriceSets = variantPriceSets.reduce( - (acc, { id, price }) => { - const calculatedPriceSet = idToPriceSet.get(price?.price_set_id) + (acc, { id, price_set }) => { + const calculatedPriceSet = idToPriceSet.get(price_set?.id) if (calculatedPriceSet) { acc[id] = calculatedPriceSet } diff --git a/packages/core-flows/src/pricing/steps/create-price-sets.ts b/packages/core-flows/src/pricing/steps/create-price-sets.ts new file mode 100644 index 0000000000..dc8ddf1c9e --- /dev/null +++ b/packages/core-flows/src/pricing/steps/create-price-sets.ts @@ -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( + 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( + ModuleRegistrationName.PRICING + ) + + await pricingModule.delete(priceSets) + } +) diff --git a/packages/core-flows/src/pricing/steps/index.ts b/packages/core-flows/src/pricing/steps/index.ts index 551d2fee21..ac84365e0e 100644 --- a/packages/core-flows/src/pricing/steps/index.ts +++ b/packages/core-flows/src/pricing/steps/index.ts @@ -1,3 +1,5 @@ +export * from "./create-price-sets" +export * from "./update-price-sets" export * from "./create-pricing-rule-types" export * from "./delete-pricing-rule-types" export * from "./update-pricing-rule-types" diff --git a/packages/core-flows/src/pricing/steps/update-price-sets.ts b/packages/core-flows/src/pricing/steps/update-price-sets.ts new file mode 100644 index 0000000000..ec54f0e1ac --- /dev/null +++ b/packages/core-flows/src/pricing/steps/update-price-sets.ts @@ -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( + 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( + ModuleRegistrationName.PRICING + ) + + await pricingModule.update( + dataBeforeUpdate.map((data) => + convertItemResponseToUpdateRequest(data, selects, relations) + ) + ) + } +) diff --git a/packages/core-flows/src/product/steps/create-product-variants.ts b/packages/core-flows/src/product/steps/create-product-variants.ts index 0eb2f2f96b..3167e0c6fb 100644 --- a/packages/core-flows/src/product/steps/create-product-variants.ts +++ b/packages/core-flows/src/product/steps/create-product-variants.ts @@ -9,7 +9,6 @@ export const createProductVariantsStep = createStep( const service = container.resolve( ModuleRegistrationName.PRODUCT ) - const created = await service.createVariants(data) return new StepResponse( created, diff --git a/packages/core-flows/src/product/steps/create-variant-pricing-link.ts b/packages/core-flows/src/product/steps/create-variant-pricing-link.ts new file mode 100644 index 0000000000..d47e52f1ea --- /dev/null +++ b/packages/core-flows/src/product/steps/create-variant-pricing-link.ts @@ -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) + } +) diff --git a/packages/core-flows/src/product/steps/get-products.ts b/packages/core-flows/src/product/steps/get-products.ts new file mode 100644 index 0000000000..615bc1af31 --- /dev/null +++ b/packages/core-flows/src/product/steps/get-products.ts @@ -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( + ModuleRegistrationName.PRODUCT + ) + + const products = await service.list( + { id: data.ids }, + { relations: ["variants"], take: null } + ) + return new StepResponse(products, products) + } +) diff --git a/packages/core-flows/src/product/steps/index.ts b/packages/core-flows/src/product/steps/index.ts index f01edde6af..2d0eee8705 100644 --- a/packages/core-flows/src/product/steps/index.ts +++ b/packages/core-flows/src/product/steps/index.ts @@ -1,6 +1,9 @@ export * from "./create-products" export * from "./update-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 "./update-product-options" export * from "./delete-product-options" diff --git a/packages/core-flows/src/product/steps/remove-variant-pricing-link.ts b/packages/core-flows/src/product/steps/remove-variant-pricing-link.ts new file mode 100644 index 0000000000..d65772fe68 --- /dev/null +++ b/packages/core-flows/src/product/steps/remove-variant-pricing-link.ts @@ -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, + }, + })) + ) + } +) diff --git a/packages/core-flows/src/product/workflows/create-product-variants.ts b/packages/core-flows/src/product/workflows/create-product-variants.ts index aa8df1b2c4..62363ea4ea 100644 --- a/packages/core-flows/src/product/workflows/create-product-variants.ts +++ b/packages/core-flows/src/product/workflows/create-product-variants.ts @@ -1,9 +1,20 @@ -import { ProductTypes } from "@medusajs/types" -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { createProductVariantsStep } from "../steps" +import { ProductTypes, PricingTypes } from "@medusajs/types" +import { + 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 = { - product_variants: ProductTypes.CreateProductVariantDTO[] + product_variants: (ProductTypes.CreateProductVariantDTO & { + prices?: PricingTypes.CreateMoneyAmountDTO[] + })[] } export const createProductVariantsWorkflowId = "create-product-variants" @@ -12,6 +23,71 @@ export const createProductVariantsWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowData => { - 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, + })) + } + ) } ) diff --git a/packages/core-flows/src/product/workflows/create-products.ts b/packages/core-flows/src/product/workflows/create-products.ts index 1708ba5a7c..81e759f054 100644 --- a/packages/core-flows/src/product/workflows/create-products.ts +++ b/packages/core-flows/src/product/workflows/create-products.ts @@ -1,8 +1,21 @@ -import { ProductTypes } from "@medusajs/types" -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { createProductsStep } from "../steps" +import { ProductTypes, PricingTypes } from "@medusajs/types" +import { + 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 & { + variants?: (ProductTypes.CreateProductVariantDTO & { + prices?: PricingTypes.CreateMoneyAmountDTO[] + })[] + })[] +} export const createProductsWorkflowId = "create-products" export const createProductsWorkflow = createWorkflow( @@ -10,6 +23,78 @@ export const createProductsWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowData => { - 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, + })), + })) + } + ) } ) diff --git a/packages/core-flows/src/product/workflows/delete-product-variants.ts b/packages/core-flows/src/product/workflows/delete-product-variants.ts index af267afcbb..d46adeaffd 100644 --- a/packages/core-flows/src/product/workflows/delete-product-variants.ts +++ b/packages/core-flows/src/product/workflows/delete-product-variants.ts @@ -1,5 +1,8 @@ import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { deleteProductVariantsStep } from "../steps" +import { + deleteProductVariantsStep, + removeVariantPricingLinkStep, +} from "../steps" type WorkflowInput = { ids: string[] } @@ -7,6 +10,8 @@ export const deleteProductVariantsWorkflowId = "delete-product-variants" export const deleteProductVariantsWorkflow = createWorkflow( deleteProductVariantsWorkflowId, (input: WorkflowData): WorkflowData => { + // Question: Should we also remove the price set manually, or would that be cascaded? + removeVariantPricingLinkStep({ variant_ids: input.ids }) return deleteProductVariantsStep(input.ids) } ) diff --git a/packages/core-flows/src/product/workflows/delete-products.ts b/packages/core-flows/src/product/workflows/delete-products.ts index 8e10c31c63..c08458dec6 100644 --- a/packages/core-flows/src/product/workflows/delete-products.ts +++ b/packages/core-flows/src/product/workflows/delete-products.ts @@ -1,5 +1,13 @@ -import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { deleteProductsStep } from "../steps" +import { + WorkflowData, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { + deleteProductsStep, + getProductsStep, + removeVariantPricingLinkStep, +} from "../steps" type WorkflowInput = { ids: string[] } @@ -7,6 +15,17 @@ export const deleteProductsWorkflowId = "delete-products" export const deleteProductsWorkflow = createWorkflow( deleteProductsWorkflowId, (input: WorkflowData): WorkflowData => { + 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) } ) diff --git a/packages/link-modules/src/definitions/product-variant-price-set.ts b/packages/link-modules/src/definitions/product-variant-price-set.ts index 19458f3c28..26b771bec3 100644 --- a/packages/link-modules/src/definitions/product-variant-price-set.ts +++ b/packages/link-modules/src/definitions/product-variant-price-set.ts @@ -41,11 +41,14 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = { extends: [ { serviceName: Modules.PRODUCT, + fieldAlias: { + price_set: "price_set_link.price_set", + }, relationship: { serviceName: LINKS.ProductVariantPriceSet, primaryKey: "variant_id", foreignKey: "id", - alias: "price", + alias: "price_set_link", }, }, { diff --git a/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts index 9f0bf22d5e..2853c0383b 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts @@ -25,7 +25,7 @@ export const GET = async ( const queryObject = remoteQueryObjectFromString({ entryPoint: "product_option", variables, - fields: req.retrieveConfig.select as string[], + fields: req.remoteQueryConfig.fields, }) const [product_option] = await remoteQuery(queryObject) diff --git a/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts index 5d3977f8fd..3623af5f5d 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts @@ -22,7 +22,7 @@ export const GET = async ( skip: req.listConfig.skip, take: req.listConfig.take, }, - fields: req.listConfig.select as string[], + fields: req.remoteQueryConfig.fields, }) const { rows: product_options, metadata } = await remoteQuery(queryObject) diff --git a/packages/medusa/src/api-v2/admin/products/[id]/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/route.ts index edfda00481..c0db29b9a7 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/route.ts @@ -9,6 +9,7 @@ import { import { UpdateProductDTO } from "@medusajs/types" import { remoteQueryObjectFromString } from "@medusajs/utils" +import { remapKeysForProduct, remapProduct } from "../helpers" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -18,15 +19,16 @@ export const GET = async ( const variables = { id: req.params.id } + const selectFields = remapKeysForProduct(req.remoteQueryConfig.fields ?? []) const queryObject = remoteQueryObjectFromString({ entryPoint: "product", variables, - fields: req.retrieveConfig.select as string[], + fields: selectFields, }) const [product] = await remoteQuery(queryObject) - res.status(200).json({ product }) + res.status(200).json({ product: remapProduct(product) }) } export const POST = async ( @@ -45,7 +47,7 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ product: result[0] }) + res.status(200).json({ product: remapProduct(result[0]) }) } export const DELETE = async ( diff --git a/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts index c57ab87bca..4c5a84bd1b 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts @@ -10,6 +10,7 @@ import { import { UpdateProductVariantDTO } from "@medusajs/types" import { defaultAdminProductsVariantFields } from "../../../query-config" import { remoteQueryObjectFromString } from "@medusajs/utils" +import { remapKeysForVariant, remapVariant } from "../../../helpers" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -26,11 +27,11 @@ export const GET = async ( const queryObject = remoteQueryObjectFromString({ entryPoint: "variant", variables, - fields: req.retrieveConfig.select as string[], + fields: remapKeysForVariant(req.remoteQueryConfig.fields ?? []), }) const [variant] = await remoteQuery(queryObject) - res.status(200).json({ variant }) + res.status(200).json({ variant: remapVariant(variant) }) } export const POST = async ( @@ -55,7 +56,7 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ variant: result[0] }) + res.status(200).json({ variant: remapVariant(result[0]) }) } export const DELETE = async ( diff --git a/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts index 7aa58c50d0..dae7619a5a 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts @@ -6,6 +6,12 @@ import { import { CreateProductVariantDTO } from "@medusajs/types" import { createProductVariantsWorkflow } from "@medusajs/core-flows" import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + remapKeysForProduct, + remapKeysForVariant, + remapProduct, + remapVariant, +} from "../../helpers" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -22,13 +28,13 @@ export const GET = async ( skip: req.listConfig.skip, take: req.listConfig.take, }, - fields: req.listConfig.select as string[], + fields: remapKeysForVariant(req.remoteQueryConfig.fields ?? []), }) const { rows: variants, metadata } = await remoteQuery(queryObject) res.json({ - variants, + variants: variants.map(remapVariant), count: metadata.count, offset: metadata.skip, limit: metadata.take, @@ -58,5 +64,15 @@ export const POST = async ( 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]) }) } diff --git a/packages/medusa/src/api-v2/admin/products/helpers.ts b/packages/medusa/src/api-v2/admin/products/helpers.ts new file mode 100644 index 0000000000..14180280c3 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/products/helpers.ts @@ -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, + } +} diff --git a/packages/medusa/src/api-v2/admin/products/middlewares.ts b/packages/medusa/src/api-v2/admin/products/middlewares.ts index 85d5306e39..b5c5e6ce36 100644 --- a/packages/medusa/src/api-v2/admin/products/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/products/middlewares.ts @@ -83,7 +83,14 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [ { method: ["POST"], 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"], diff --git a/packages/medusa/src/api-v2/admin/products/query-config.ts b/packages/medusa/src/api-v2/admin/products/query-config.ts index 1c0703948f..8d7312a7cd 100644 --- a/packages/medusa/src/api-v2/admin/products/query-config.ts +++ b/packages/medusa/src/api-v2/admin/products/query-config.ts @@ -22,6 +22,11 @@ export const defaultAdminProductsVariantFields = [ "ean", "upc", "barcode", + "prices.id", + "prices.currency_code", + "prices.amount", + "prices.created_at", + "prices.updated_at", "options.id", "options.option_value.value", "options.option_value.option.title", @@ -55,7 +60,6 @@ export const listOptionConfig = { /* export const allowedAdminProductRelations = [ "variants", - // TODO: Add in next iteration // "variants.prices", "variants.options", "images", diff --git a/packages/medusa/src/api-v2/admin/products/route.ts b/packages/medusa/src/api-v2/admin/products/route.ts index c25a76cbed..26af21af89 100644 --- a/packages/medusa/src/api-v2/admin/products/route.ts +++ b/packages/medusa/src/api-v2/admin/products/route.ts @@ -11,13 +11,13 @@ import { } from "../../../types/routing" import { listPriceLists } from "../price-lists/queries" import { AdminGetProductsParams } from "./validators" +import { remapKeysForProduct, remapProduct } from "./helpers" +import { MedusaContainer } from "medusa-core-utils" -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse +const applyVariantFiltersForPriceList = async ( + scope: MedusaContainer, + filterableFields: AdminGetProductsParams ) => { - const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) - const filterableFields: AdminGetProductsParams = { ...req.filterableFields } const filterByPriceListIds = filterableFields.price_list_id const priceListVariantIds: string[] = [] @@ -25,17 +25,17 @@ export const GET = async ( // the variant IDs through the price list price sets. if (Array.isArray(filterByPriceListIds)) { const [priceLists] = await listPriceLists({ - container: req.scope, + container: scope, remoteQueryFields: ["price_set_money_amounts.price_set.variant.id"], apiFields: ["prices.variant_id"], variables: { filters: { id: filterByPriceListIds }, skip: 0, take: null }, }) priceListVariantIds.push( - ...(priceLists + ...((priceLists .map((priceList) => priceList.prices?.map((price) => price.variant_id)) .flat(2) - .filter(isString) || []) + .filter(isString) || []) as string[]) ) delete filterableFields.price_list_id @@ -50,19 +50,34 @@ export const GET = async ( } } + return filterableFields +} + +export const GET = async ( + req: AuthenticatedMedusaRequest, + 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({ entryPoint: "product", variables: { filters: filterableFields, ...req.remoteQueryConfig.pagination, }, - fields: req.remoteQueryConfig.fields, + fields: selectFields, }) const { rows: products, metadata } = await remoteQuery(queryObject) res.json({ - products, + products: products.map(remapProduct), count: metadata.count, offset: metadata.skip, limit: metadata.take, @@ -88,5 +103,5 @@ export const POST = async ( throw errors[0].error } - res.status(200).json({ product: result[0] }) + res.status(200).json({ product: remapProduct(result[0]) }) } diff --git a/packages/medusa/src/api-v2/admin/products/validators.ts b/packages/medusa/src/api-v2/admin/products/validators.ts index d34ffc5ecf..837c5adc60 100644 --- a/packages/medusa/src/api-v2/admin/products/validators.ts +++ b/packages/medusa/src/api-v2/admin/products/validators.ts @@ -5,11 +5,13 @@ import { IsArray, IsBoolean, IsEnum, + IsInt, IsNumber, IsObject, IsOptional, IsString, NotEquals, + Validate, ValidateIf, ValidateNested, } from "class-validator" @@ -17,6 +19,7 @@ import { FindParams, extendedFindParamsMixin } from "../../../types/common" import { OperatorMapValidator } from "../../../types/validators/operator-map" import { IsType } from "../../../utils" import { optionalBooleanMapper } from "../../../utils/validators/is-boolean" +import { XorConstraint } from "../../../types/validators/xor" export class AdminGetProductsProductParams extends FindParams {} export class AdminGetProductsProductVariantsVariantParams extends FindParams {} @@ -537,13 +540,10 @@ export class AdminPostProductsProductVariantsReq { @IsOptional() metadata?: Record - // TODO: Add on next iteration, adding temporary field for now - // @IsArray() - // @ValidateNested({ each: true }) - // @Type(() => ProductVariantPricesCreateReq) - // prices: ProductVariantPricesCreateReq[] @IsArray() - prices: any[] + @ValidateNested({ each: true }) + @Type(() => ProductVariantPricesCreateReq) + prices: ProductVariantPricesCreateReq[] @IsOptional() @IsObject() @@ -619,12 +619,11 @@ export class AdminPostProductsProductVariantsVariantReq { @IsOptional() metadata?: Record - // TODO: Deal with in next iteration - // @IsArray() - // @IsOptional() - // @ValidateNested({ each: true }) - // @Type(() => ProductVariantPricesUpdateReq) - // prices?: ProductVariantPricesUpdateReq[] + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => ProductVariantPricesUpdateReq) + prices?: ProductVariantPricesUpdateReq[] @IsOptional() @IsObject() @@ -679,3 +678,41 @@ export class ProductTypeReq { @IsString() 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 +} diff --git a/packages/types/src/pricing/common/price-set.ts b/packages/types/src/pricing/common/price-set.ts index 2752e29d08..94bee56e80 100644 --- a/packages/types/src/pricing/common/price-set.ts +++ b/packages/types/src/pricing/common/price-set.ts @@ -1,5 +1,5 @@ -import { BaseFilterable } from "../../dal"; -import { CreatePriceSetPriceRules } from "./price-list"; +import { BaseFilterable } from "../../dal" +import { CreatePriceSetPriceRules } from "./price-list" import { CreateMoneyAmountDTO, FilterableMoneyAmountProps,