Skip to content

Commit 6ca755e

Browse files
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>
1 parent fbf3388 commit 6ca755e

4 files changed

Lines changed: 255 additions & 4 deletions

File tree

.changeset/blue-bags-grin.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"integration-tests-http": patch
3+
"@medusajs/types": patch
4+
"@medusajs/medusa": patch
5+
---
6+
7+
Enable filtering admin products API by variant EAN, UPC, and barcode

integration-tests/http/__tests__/product/admin/product.spec.ts

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,231 @@ medusaIntegrationTestRunner({
949949
}),
950950
])
951951
})
952+
953+
it("returns a list of products filtered by variants[ean]", async () => {
954+
const productWithEan = await api.post(
955+
"/admin/products",
956+
getProductFixture({
957+
title: "Product with EAN",
958+
shipping_profile_id: shippingProfile.id,
959+
variants: [
960+
{
961+
title: "Test variant",
962+
ean: "1234567890123",
963+
prices: [{ currency_code: "usd", amount: 100 }],
964+
options: {
965+
size: "large",
966+
color: "green",
967+
},
968+
},
969+
],
970+
}),
971+
adminHeaders
972+
)
973+
974+
await api.post(
975+
"/admin/products",
976+
getProductFixture({
977+
title: "Product with different EAN",
978+
shipping_profile_id: shippingProfile.id,
979+
variants: [
980+
{
981+
title: "Test variant 2",
982+
ean: "9876543210987",
983+
prices: [{ currency_code: "usd", amount: 150 }],
984+
options: {
985+
size: "large",
986+
color: "green",
987+
},
988+
},
989+
],
990+
}),
991+
adminHeaders
992+
)
993+
994+
const response = await api
995+
.get("/admin/products?variants[ean]=1234567890123", adminHeaders)
996+
.catch((err) => {
997+
console.log(err)
998+
})
999+
1000+
expect(response.status).toEqual(200)
1001+
expect(response.data.products).toHaveLength(1)
1002+
expect(response.data.products).toEqual(
1003+
expect.arrayContaining([
1004+
expect.objectContaining({
1005+
id: productWithEan.data.product.id,
1006+
title: "Product with EAN",
1007+
variants: expect.arrayContaining([
1008+
expect.objectContaining({
1009+
ean: "1234567890123",
1010+
}),
1011+
]),
1012+
}),
1013+
])
1014+
)
1015+
})
1016+
1017+
it("returns a list of products filtered by variants[upc]", async () => {
1018+
const productWithUpc = await api.post(
1019+
"/admin/products",
1020+
getProductFixture({
1021+
title: "Product with UPC",
1022+
shipping_profile_id: shippingProfile.id,
1023+
variants: [
1024+
{
1025+
title: "Test variant",
1026+
upc: "123456789012",
1027+
prices: [{ currency_code: "usd", amount: 200 }],
1028+
options: {
1029+
size: "large",
1030+
color: "green",
1031+
},
1032+
},
1033+
],
1034+
}),
1035+
adminHeaders
1036+
)
1037+
1038+
await api.post(
1039+
"/admin/products",
1040+
getProductFixture({
1041+
title: "Product with different UPC",
1042+
shipping_profile_id: shippingProfile.id,
1043+
variants: [
1044+
{
1045+
title: "Test variant 2",
1046+
upc: "098765432109",
1047+
prices: [{ currency_code: "usd", amount: 250 }],
1048+
options: {
1049+
size: "large",
1050+
color: "green",
1051+
},
1052+
},
1053+
],
1054+
}),
1055+
adminHeaders
1056+
)
1057+
1058+
const response = await api
1059+
.get("/admin/products?variants[upc]=123456789012", adminHeaders)
1060+
.catch((err) => {
1061+
console.log(err)
1062+
})
1063+
1064+
expect(response.status).toEqual(200)
1065+
expect(response.data.products).toHaveLength(1)
1066+
expect(response.data.products).toEqual(
1067+
expect.arrayContaining([
1068+
expect.objectContaining({
1069+
id: productWithUpc.data.product.id,
1070+
title: "Product with UPC",
1071+
variants: expect.arrayContaining([
1072+
expect.objectContaining({
1073+
upc: "123456789012",
1074+
}),
1075+
]),
1076+
}),
1077+
])
1078+
)
1079+
})
1080+
1081+
it("returns a list of products filtered by variants[barcode]", async () => {
1082+
const productWithBarcode = await api.post(
1083+
"/admin/products",
1084+
getProductFixture({
1085+
title: "Product with Barcode",
1086+
shipping_profile_id: shippingProfile.id,
1087+
variants: [
1088+
{
1089+
title: "Test variant",
1090+
barcode: "1234567890",
1091+
prices: [{ currency_code: "usd", amount: 300 }],
1092+
options: {
1093+
size: "large",
1094+
color: "green",
1095+
},
1096+
},
1097+
],
1098+
}),
1099+
adminHeaders
1100+
)
1101+
1102+
await api.post(
1103+
"/admin/products",
1104+
getProductFixture({
1105+
title: "Product with different Barcode",
1106+
shipping_profile_id: shippingProfile.id,
1107+
variants: [
1108+
{
1109+
title: "Test variant 2",
1110+
barcode: "0987654321",
1111+
prices: [{ currency_code: "usd", amount: 350 }],
1112+
options: {
1113+
size: "large",
1114+
color: "green",
1115+
},
1116+
},
1117+
],
1118+
}),
1119+
adminHeaders
1120+
)
1121+
1122+
const response = await api
1123+
.get("/admin/products?variants[barcode]=1234567890", adminHeaders)
1124+
.catch((err) => {
1125+
console.log(err)
1126+
})
1127+
1128+
expect(response.status).toEqual(200)
1129+
expect(response.data.products).toHaveLength(1)
1130+
expect(response.data.products).toEqual(
1131+
expect.arrayContaining([
1132+
expect.objectContaining({
1133+
id: productWithBarcode.data.product.id,
1134+
title: "Product with Barcode",
1135+
variants: expect.arrayContaining([
1136+
expect.objectContaining({
1137+
barcode: "1234567890",
1138+
}),
1139+
]),
1140+
}),
1141+
])
1142+
)
1143+
})
1144+
1145+
it("returns empty list when filtering by non-existent variants[ean]", async () => {
1146+
const response = await api
1147+
.get("/admin/products?variants[ean]=5555555555555", adminHeaders)
1148+
.catch((err) => {
1149+
console.log(err)
1150+
})
1151+
1152+
expect(response.status).toEqual(200)
1153+
expect(response.data.products).toHaveLength(0)
1154+
})
1155+
1156+
it("returns empty list when filtering by non-existent variants[upc]", async () => {
1157+
const response = await api
1158+
.get("/admin/products?variants[upc]=555555555555", adminHeaders)
1159+
.catch((err) => {
1160+
console.log(err)
1161+
})
1162+
1163+
expect(response.status).toEqual(200)
1164+
expect(response.data.products).toHaveLength(0)
1165+
})
1166+
1167+
it("returns empty list when filtering by non-existent variants[barcode]", async () => {
1168+
const response = await api
1169+
.get("/admin/products?variants[barcode]=5555555555", adminHeaders)
1170+
.catch((err) => {
1171+
console.log(err)
1172+
})
1173+
1174+
expect(response.status).toEqual(200)
1175+
expect(response.data.products).toHaveLength(0)
1176+
})
9521177
})
9531178

9541179
describe("GET /admin/products/:id", () => {

packages/core/types/src/http/product/admin/queries.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export interface AdminProductVariantParams
2424
* out of stock.
2525
*/
2626
allow_backorder?: boolean
27+
/**
28+
* Filter by variant ean(s).
29+
*/
30+
ean?: string | string[]
31+
/**
32+
* Filter by variant upc(s).
33+
*/
34+
upc?: string | string[]
35+
/**
36+
* Filter by variant barcode(s).
37+
*/
38+
barcode?: string | string[]
2739
/**
2840
* Apply filters on the variant's creation date.
2941
*/
@@ -46,5 +58,5 @@ export interface AdminProductListParams
4658
/**
4759
* Apply filters on the product variants.
4860
*/
49-
variants?: AdminProductVariantParams
61+
variants?: Omit<AdminProductVariantParams, "q">
5062
}

packages/medusa/src/api/admin/products/validators.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export const AdminGetProductVariantsParamsFields = z.object({
2525
id: z.union([z.string(), z.array(z.string())]).optional(),
2626
manage_inventory: booleanString().optional(),
2727
allow_backorder: booleanString().optional(),
28+
ean: z.union([z.string(), z.array(z.string())]).optional(),
29+
upc: z.union([z.string(), z.array(z.string())]).optional(),
30+
barcode: z.union([z.string(), z.array(z.string())]).optional(),
2831
created_at: createOperatorMap().optional(),
2932
updated_at: createOperatorMap().optional(),
3033
deleted_at: createOperatorMap().optional(),
@@ -41,9 +44,13 @@ export const AdminGetProductVariantsParams = createFindParams({
4144
.merge(applyAndAndOrOperators(AdminGetProductVariantsParamsFields))
4245

4346
export const AdminGetProductsParamsDirectFields = z.object({
44-
variants: AdminGetProductVariantsParamsFields.merge(
45-
applyAndAndOrOperators(AdminGetProductVariantsParamsFields)
46-
).optional(),
47+
variants: AdminGetProductVariantsParamsFields.omit({ q: true })
48+
.merge(
49+
applyAndAndOrOperators(
50+
AdminGetProductVariantsParamsFields.omit({ q: true })
51+
)
52+
)
53+
.optional(),
4754
status: statusEnum.array().optional(),
4855
})
4956

0 commit comments

Comments
 (0)