feat: Enable filtering admin products by variant EAN, UPC, and barcode (#12815)

* Add filters for variant ean, upc, and barcode in product queries and validators

* fix: Omit 'q' field from variants in product list and validation parameters

* Add tests for admin products filtering by variants ean, upc, and barcode

* Add changeset for filter admin products api by variant ean, upc, and barcode

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Ante Primorac
2025-06-25 09:17:01 +02:00
committed by GitHub
parent fbf33885f5
commit 6ca755ede7
4 changed files with 255 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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