diff --git a/.changeset/blue-bags-grin.md b/.changeset/blue-bags-grin.md new file mode 100644 index 0000000000..14d0fde06b --- /dev/null +++ b/.changeset/blue-bags-grin.md @@ -0,0 +1,7 @@ +--- +"integration-tests-http": patch +"@medusajs/types": patch +"@medusajs/medusa": patch +--- + +Enable filtering admin products API by variant EAN, UPC, and barcode diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts index b9144823dc..f92456b01c 100644 --- a/integration-tests/http/__tests__/product/admin/product.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product.spec.ts @@ -949,6 +949,231 @@ medusaIntegrationTestRunner({ }), ]) }) + + it("returns a list of products filtered by variants[ean]", async () => { + const productWithEan = await api.post( + "/admin/products", + getProductFixture({ + title: "Product with EAN", + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant", + ean: "1234567890123", + prices: [{ currency_code: "usd", amount: 100 }], + options: { + size: "large", + color: "green", + }, + }, + ], + }), + adminHeaders + ) + + await api.post( + "/admin/products", + getProductFixture({ + title: "Product with different EAN", + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant 2", + ean: "9876543210987", + prices: [{ currency_code: "usd", amount: 150 }], + options: { + size: "large", + color: "green", + }, + }, + ], + }), + adminHeaders + ) + + const response = await api + .get("/admin/products?variants[ean]=1234567890123", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: productWithEan.data.product.id, + title: "Product with EAN", + variants: expect.arrayContaining([ + expect.objectContaining({ + ean: "1234567890123", + }), + ]), + }), + ]) + ) + }) + + it("returns a list of products filtered by variants[upc]", async () => { + const productWithUpc = await api.post( + "/admin/products", + getProductFixture({ + title: "Product with UPC", + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant", + upc: "123456789012", + prices: [{ currency_code: "usd", amount: 200 }], + options: { + size: "large", + color: "green", + }, + }, + ], + }), + adminHeaders + ) + + await api.post( + "/admin/products", + getProductFixture({ + title: "Product with different UPC", + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant 2", + upc: "098765432109", + prices: [{ currency_code: "usd", amount: 250 }], + options: { + size: "large", + color: "green", + }, + }, + ], + }), + adminHeaders + ) + + const response = await api + .get("/admin/products?variants[upc]=123456789012", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: productWithUpc.data.product.id, + title: "Product with UPC", + variants: expect.arrayContaining([ + expect.objectContaining({ + upc: "123456789012", + }), + ]), + }), + ]) + ) + }) + + it("returns a list of products filtered by variants[barcode]", async () => { + const productWithBarcode = await api.post( + "/admin/products", + getProductFixture({ + title: "Product with Barcode", + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant", + barcode: "1234567890", + prices: [{ currency_code: "usd", amount: 300 }], + options: { + size: "large", + color: "green", + }, + }, + ], + }), + adminHeaders + ) + + await api.post( + "/admin/products", + getProductFixture({ + title: "Product with different Barcode", + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant 2", + barcode: "0987654321", + prices: [{ currency_code: "usd", amount: 350 }], + options: { + size: "large", + color: "green", + }, + }, + ], + }), + adminHeaders + ) + + const response = await api + .get("/admin/products?variants[barcode]=1234567890", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: productWithBarcode.data.product.id, + title: "Product with Barcode", + variants: expect.arrayContaining([ + expect.objectContaining({ + barcode: "1234567890", + }), + ]), + }), + ]) + ) + }) + + it("returns empty list when filtering by non-existent variants[ean]", async () => { + const response = await api + .get("/admin/products?variants[ean]=5555555555555", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(0) + }) + + it("returns empty list when filtering by non-existent variants[upc]", async () => { + const response = await api + .get("/admin/products?variants[upc]=555555555555", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(0) + }) + + it("returns empty list when filtering by non-existent variants[barcode]", async () => { + const response = await api + .get("/admin/products?variants[barcode]=5555555555", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(0) + }) }) describe("GET /admin/products/:id", () => { diff --git a/packages/core/types/src/http/product/admin/queries.ts b/packages/core/types/src/http/product/admin/queries.ts index 93d4138d10..ab7520ff99 100644 --- a/packages/core/types/src/http/product/admin/queries.ts +++ b/packages/core/types/src/http/product/admin/queries.ts @@ -24,6 +24,18 @@ export interface AdminProductVariantParams * out of stock. */ allow_backorder?: boolean + /** + * Filter by variant ean(s). + */ + ean?: string | string[] + /** + * Filter by variant upc(s). + */ + upc?: string | string[] + /** + * Filter by variant barcode(s). + */ + barcode?: string | string[] /** * Apply filters on the variant's creation date. */ @@ -46,5 +58,5 @@ export interface AdminProductListParams /** * Apply filters on the product variants. */ - variants?: AdminProductVariantParams + variants?: Omit } diff --git a/packages/medusa/src/api/admin/products/validators.ts b/packages/medusa/src/api/admin/products/validators.ts index ba0e99e583..589c7be7ba 100644 --- a/packages/medusa/src/api/admin/products/validators.ts +++ b/packages/medusa/src/api/admin/products/validators.ts @@ -25,6 +25,9 @@ export const AdminGetProductVariantsParamsFields = z.object({ id: z.union([z.string(), z.array(z.string())]).optional(), manage_inventory: booleanString().optional(), allow_backorder: booleanString().optional(), + ean: z.union([z.string(), z.array(z.string())]).optional(), + upc: z.union([z.string(), z.array(z.string())]).optional(), + barcode: z.union([z.string(), z.array(z.string())]).optional(), created_at: createOperatorMap().optional(), updated_at: createOperatorMap().optional(), deleted_at: createOperatorMap().optional(), @@ -41,9 +44,13 @@ export const AdminGetProductVariantsParams = createFindParams({ .merge(applyAndAndOrOperators(AdminGetProductVariantsParamsFields)) export const AdminGetProductsParamsDirectFields = z.object({ - variants: AdminGetProductVariantsParamsFields.merge( - applyAndAndOrOperators(AdminGetProductVariantsParamsFields) - ).optional(), + variants: AdminGetProductVariantsParamsFields.omit({ q: true }) + .merge( + applyAndAndOrOperators( + AdminGetProductVariantsParamsFields.omit({ q: true }) + ) + ) + .optional(), status: statusEnum.array().optional(), })