Merge pull request #373 from medusajs/feat/product-variant-rank

Feat: Add product variant rank
This commit is contained in:
pKorsholm
2021-09-09 09:58:27 +02:00
committed by GitHub
15 changed files with 1229 additions and 153 deletions
@@ -0,0 +1,382 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/admin/products GET /admin/products returns a list of products with child entities 1`] = `
Array [
Object {
"collection": Object {
"created_at": Any<String>,
"deleted_at": null,
"handle": "test-collection",
"id": StringMatching /\\^test-\\*/,
"metadata": null,
"title": "Test collection",
"updated_at": Any<String>,
},
"collection_id": "test-collection",
"created_at": Any<String>,
"deleted_at": null,
"description": "test-product-description",
"discountable": true,
"handle": "test-product",
"height": null,
"hs_code": null,
"id": StringMatching /\\^test-\\*/,
"images": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-\\*/,
"metadata": null,
"updated_at": Any<String>,
"url": "test-image.png",
},
],
"is_giftcard": false,
"length": null,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-\\*/,
"metadata": null,
"product_id": StringMatching /\\^test-\\*/,
"title": "test-option",
"updated_at": Any<String>,
},
],
"origin_country": null,
"profile_id": StringMatching /\\^sp_\\*/,
"subtitle": null,
"tags": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^tag\\*/,
"metadata": null,
"updated_at": Any<String>,
"value": "123",
},
],
"thumbnail": null,
"title": "Test product",
"type": Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-\\*/,
"metadata": null,
"updated_at": Any<String>,
"value": "test-type",
},
"type_id": "test-type",
"updated_at": Any<String>,
"variants": Array [
Object {
"allow_backorder": false,
"barcode": "test-barcode",
"created_at": Any<String>,
"deleted_at": null,
"ean": "test-ean",
"height": null,
"hs_code": null,
"id": "test-variant",
"inventory_quantity": 10,
"length": null,
"manage_inventory": true,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-variant-option\\*/,
"metadata": null,
"option_id": StringMatching /\\^test-opt\\*/,
"updated_at": Any<String>,
"value": "Default variant",
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"origin_country": null,
"prices": Array [
Object {
"amount": 100,
"created_at": Any<String>,
"currency_code": "usd",
"deleted_at": null,
"id": StringMatching /\\^test-price\\*/,
"region_id": null,
"sale_amount": null,
"updated_at": Any<String>,
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"product_id": StringMatching /\\^test-\\*/,
"sku": "test-sku",
"title": "Test variant",
"upc": "test-upc",
"updated_at": Any<String>,
"weight": null,
"width": null,
},
Object {
"allow_backorder": false,
"barcode": null,
"created_at": Any<String>,
"deleted_at": null,
"ean": "test-ean2",
"height": null,
"hs_code": null,
"id": "test-variant_2",
"inventory_quantity": 10,
"length": null,
"manage_inventory": true,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-variant-option\\*/,
"metadata": null,
"option_id": StringMatching /\\^test-opt\\*/,
"updated_at": Any<String>,
"value": "Default variant 2",
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"origin_country": null,
"prices": Array [
Object {
"amount": 100,
"created_at": Any<String>,
"currency_code": "usd",
"deleted_at": null,
"id": StringMatching /\\^test-price\\*/,
"region_id": null,
"sale_amount": null,
"updated_at": Any<String>,
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"product_id": StringMatching /\\^test-\\*/,
"sku": "test-sku2",
"title": "Test variant rank (2)",
"upc": "test-upc2",
"updated_at": Any<String>,
"weight": null,
"width": null,
},
Object {
"allow_backorder": false,
"barcode": "test-barcode 1",
"created_at": Any<String>,
"deleted_at": null,
"ean": "test-ean1",
"height": null,
"hs_code": null,
"id": "test-variant_1",
"inventory_quantity": 10,
"length": null,
"manage_inventory": true,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-variant-option\\*/,
"metadata": null,
"option_id": StringMatching /\\^test-opt\\*/,
"updated_at": Any<String>,
"value": "Default variant 1",
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"origin_country": null,
"prices": Array [
Object {
"amount": 100,
"created_at": Any<String>,
"currency_code": "usd",
"deleted_at": null,
"id": StringMatching /\\^test-price\\*/,
"region_id": null,
"sale_amount": null,
"updated_at": Any<String>,
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"product_id": StringMatching /\\^test-\\*/,
"sku": "test-sku1",
"title": "Test variant rank (1)",
"upc": "test-upc1",
"updated_at": Any<String>,
"weight": null,
"width": null,
},
],
"weight": null,
"width": null,
},
Object {
"collection": Object {
"created_at": Any<String>,
"deleted_at": null,
"handle": "test-collection",
"id": StringMatching /\\^test-\\*/,
"metadata": null,
"title": "Test collection",
"updated_at": Any<String>,
},
"collection_id": "test-collection",
"created_at": Any<String>,
"deleted_at": null,
"description": "test-product-description1",
"discountable": true,
"handle": "test-product1",
"height": null,
"hs_code": null,
"id": StringMatching /\\^test-\\*/,
"images": Array [],
"is_giftcard": false,
"length": null,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [],
"origin_country": null,
"profile_id": StringMatching /\\^sp_\\*/,
"subtitle": null,
"tags": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^tag\\*/,
"metadata": null,
"updated_at": Any<String>,
"value": "123",
},
],
"thumbnail": null,
"title": "Test product1",
"type": Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-\\*/,
"metadata": null,
"updated_at": Any<String>,
"value": "test-type",
},
"type_id": "test-type",
"updated_at": Any<String>,
"variants": Array [
Object {
"allow_backorder": false,
"barcode": null,
"created_at": Any<String>,
"deleted_at": null,
"ean": "test-ean4",
"height": null,
"hs_code": null,
"id": "test-variant_4",
"inventory_quantity": 10,
"length": null,
"manage_inventory": true,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-variant-option\\*/,
"metadata": null,
"option_id": StringMatching /\\^test-opt\\*/,
"updated_at": Any<String>,
"value": "Default variant 3",
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"origin_country": null,
"prices": Array [
Object {
"amount": 100,
"created_at": Any<String>,
"currency_code": "usd",
"deleted_at": null,
"id": StringMatching /\\^test-price\\*/,
"region_id": null,
"sale_amount": null,
"updated_at": Any<String>,
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"product_id": StringMatching /\\^test-\\*/,
"sku": "test-sku4",
"title": "Test variant rank (2)",
"upc": "test-upc4",
"updated_at": Any<String>,
"weight": null,
"width": null,
},
Object {
"allow_backorder": false,
"barcode": null,
"created_at": Any<String>,
"deleted_at": null,
"ean": "test-ean3",
"height": null,
"hs_code": null,
"id": "test-variant_3",
"inventory_quantity": 10,
"length": null,
"manage_inventory": true,
"material": null,
"metadata": null,
"mid_code": null,
"options": Array [
Object {
"created_at": Any<String>,
"deleted_at": null,
"id": StringMatching /\\^test-variant-option\\*/,
"metadata": null,
"option_id": StringMatching /\\^test-opt\\*/,
"updated_at": Any<String>,
"value": "Default variant 3",
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"origin_country": null,
"prices": Array [
Object {
"amount": 100,
"created_at": Any<String>,
"currency_code": "usd",
"deleted_at": null,
"id": StringMatching /\\^test-price\\*/,
"region_id": null,
"sale_amount": null,
"updated_at": Any<String>,
"variant_id": StringMatching /\\^test-variant\\*/,
},
],
"product_id": StringMatching /\\^test-\\*/,
"sku": "test-sku3",
"title": "Test variant rank (2)",
"upc": "test-upc3",
"updated_at": Any<String>,
"weight": null,
"width": null,
},
],
"weight": null,
"width": null,
},
]
`;
+466 -115
View File
@@ -1,49 +1,266 @@
const path = require("path"); const path = require("path")
const setupServer = require("../../../helpers/setup-server"); const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api"); const { useApi } = require("../../../helpers/use-api")
const { initDb, useDb } = require("../../../helpers/use-db"); const { initDb, useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder"); const adminSeeder = require("../../helpers/admin-seeder")
const productSeeder = require("../../helpers/product-seeder"); const productSeeder = require("../../helpers/product-seeder")
jest.setTimeout(30000); jest.setTimeout(30000)
describe("/admin/products", () => { describe("/admin/products", () => {
let medusaProcess; let medusaProcess
let dbConnection; let dbConnection
beforeAll(async () => { beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..")); const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd }); dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd }); medusaProcess = await setupServer({ cwd, verbose: true })
}); })
afterAll(async () => { afterAll(async () => {
const db = useDb(); const db = useDb()
await db.shutdown(); await db.shutdown()
medusaProcess.kill(); medusaProcess.kill()
}); })
describe("GET /admin/products", () => {
beforeEach(async () => {
try {
await productSeeder(dbConnection)
await adminSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("returns a list of products with child entities", async () => {
const api = useApi()
const response = await api
.get("/admin/products", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.data.products).toMatchSnapshot([
{
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
options: [
{
id: expect.stringMatching(/^test-*/),
product_id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
images: [
{
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
variants: [
{
id: "test-variant", //expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
product_id: expect.stringMatching(/^test-*/),
prices: [
{
id: expect.stringMatching(/^test-price*/),
variant_id: expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
options: [
{
id: expect.stringMatching(/^test-variant-option*/),
variant_id: expect.stringMatching(/^test-variant*/),
option_id: expect.stringMatching(/^test-opt*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
},
{
id: "test-variant_2", //expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
product_id: expect.stringMatching(/^test-*/),
prices: [
{
id: expect.stringMatching(/^test-price*/),
variant_id: expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
options: [
{
id: expect.stringMatching(/^test-variant-option*/),
variant_id: expect.stringMatching(/^test-variant*/),
option_id: expect.stringMatching(/^test-opt*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
},
{
id: "test-variant_1", // expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
product_id: expect.stringMatching(/^test-*/),
prices: [
{
id: expect.stringMatching(/^test-price*/),
variant_id: expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
options: [
{
id: expect.stringMatching(/^test-variant-option*/),
variant_id: expect.stringMatching(/^test-variant*/),
option_id: expect.stringMatching(/^test-opt*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
},
],
tags: [
{
id: expect.stringMatching(/^tag*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
type: {
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
collection: {
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
profile_id: expect.stringMatching(/^sp_*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
{
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
options: [],
variants: [
{
id: "test-variant_4", //expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
product_id: expect.stringMatching(/^test-*/),
prices: [
{
id: expect.stringMatching(/^test-price*/),
variant_id: expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
options: [
{
id: expect.stringMatching(/^test-variant-option*/),
variant_id: expect.stringMatching(/^test-variant*/),
option_id: expect.stringMatching(/^test-opt*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
},
{
id: "test-variant_3", //expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
product_id: expect.stringMatching(/^test-*/),
prices: [
{
id: expect.stringMatching(/^test-price*/),
variant_id: expect.stringMatching(/^test-variant*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
options: [
{
id: expect.stringMatching(/^test-variant-option*/),
variant_id: expect.stringMatching(/^test-variant*/),
option_id: expect.stringMatching(/^test-opt*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
},
],
tags: [
{
id: expect.stringMatching(/^tag*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
],
type: {
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
collection: {
id: expect.stringMatching(/^test-*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
profile_id: expect.stringMatching(/^sp_*/),
created_at: expect.any(String),
updated_at: expect.any(String),
},
])
})
})
describe("POST /admin/products", () => { describe("POST /admin/products", () => {
beforeEach(async () => { beforeEach(async () => {
try { try {
await productSeeder(dbConnection); await productSeeder(dbConnection)
await adminSeeder(dbConnection); await adminSeeder(dbConnection)
} catch (err) { } catch (err) {
console.log(err); console.log(err)
throw err; throw err
} }
}); })
afterEach(async () => { afterEach(async () => {
const db = useDb(); const db = useDb()
await db.teardown(); await db.teardown()
}); })
it("creates a product", async () => { it("creates a product", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
title: "Test product", title: "Test product",
@@ -61,7 +278,7 @@ describe("/admin/products", () => {
options: [{ value: "large" }, { value: "green" }], options: [{ value: "large" }, { value: "green" }],
}, },
], ],
}; }
const response = await api const response = await api
.post("/admin/products", payload, { .post("/admin/products", payload, {
@@ -70,11 +287,10 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200);
expect(response.status).toEqual(200)
expect(response.data.product).toEqual( expect(response.data.product).toEqual(
expect.objectContaining({ expect.objectContaining({
title: "Test product", title: "Test product",
@@ -133,11 +349,77 @@ describe("/admin/products", () => {
}), }),
], ],
}) })
); )
}); })
it("Sets variant ranks when creating a product", async () => {
const api = useApi()
const payload = {
title: "Test product - 1",
description: "test-product-description 1",
type: { value: "test-type 1" },
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 1",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
{
title: "Test variant 2",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
],
}
const creationResponse = await api
.post("/admin/products", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(creationResponse.status).toEqual(200)
const productId = creationResponse.data.product.id
const response = await api
.get(`/admin/products/${productId}`, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.data.product).toEqual(
expect.objectContaining({
title: "Test product - 1",
variants: [
expect.objectContaining({
title: "Test variant 1",
}),
expect.objectContaining({
title: "Test variant 2",
}),
],
})
)
})
it("creates a giftcard", async () => { it("creates a giftcard", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
title: "Test Giftcard", title: "Test Giftcard",
@@ -151,7 +433,7 @@ describe("/admin/products", () => {
options: [{ value: "100" }], options: [{ value: "100" }],
}, },
], ],
}; }
const response = await api const response = await api
.post("/admin/products", payload, { .post("/admin/products", payload, {
@@ -160,21 +442,21 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.product).toEqual( expect(response.data.product).toEqual(
expect.objectContaining({ expect.objectContaining({
title: "Test Giftcard", title: "Test Giftcard",
discountable: false, discountable: false,
}) })
); )
}); })
it("updates a product (update prices, tags, delete collection, delete type, replaces images)", async () => { it("updates a product (update prices, tags, delete collection, delete type, replaces images)", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
collection_id: null, collection_id: null,
@@ -182,13 +464,19 @@ describe("/admin/products", () => {
variants: [ variants: [
{ {
id: "test-variant", id: "test-variant",
prices: [{ currency_code: "usd", amount: 100, sale_amount: 75 }], prices: [
{
currency_code: "usd",
amount: 100,
sale_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" },
}; }
const response = await api const response = await api
.post("/admin/products/test-product", payload, { .post("/admin/products/test-product", payload, {
@@ -197,10 +485,10 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.product).toEqual( expect(response.data.product).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -231,15 +519,69 @@ describe("/admin/products", () => {
value: "test-type-2", value: "test-type-2",
}), }),
}) })
); )
}); })
it("updates a product (variant ordering)", async () => {
const api = useApi()
const payload = {
collection_id: null,
type: null,
variants: [
{
id: "test-variant",
},
{
id: "test-variant_1",
},
{
id: "test-variant_2",
},
],
}
const response = await api
.post("/admin/products/test-product", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
title: "Test product",
variants: [
expect.objectContaining({
id: "test-variant",
title: "Test variant",
}),
expect.objectContaining({
id: "test-variant_1",
title: "Test variant rank (1)",
}),
expect.objectContaining({
id: "test-variant_2",
title: "Test variant rank (2)",
}),
],
type: null,
collection: null,
})
)
})
it("add option", async () => { it("add option", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
title: "should_add", title: "should_add",
}; }
const response = await api const response = await api
.post("/admin/products/test-product/options", payload, { .post("/admin/products/test-product/options", payload, {
@@ -248,41 +590,41 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.product).toEqual( expect(response.data.product).toEqual(
expect.objectContaining({ expect.objectContaining({
options: [ options: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
title: "should_add", title: "should_add",
product_id: "test-product", product_id: "test-product",
}), }),
], ]),
}) })
); )
}); })
}); })
describe("testing for soft-deletion + uniqueness on handles, collection and variant properties", () => { describe("testing for soft-deletion + uniqueness on handles, collection and variant properties", () => {
beforeEach(async () => { beforeEach(async () => {
try { try {
await productSeeder(dbConnection); await productSeeder(dbConnection)
await adminSeeder(dbConnection); await adminSeeder(dbConnection)
} catch (err) { } catch (err) {
console.log(err); console.log(err)
throw err; throw err
} }
}); })
afterEach(async () => { afterEach(async () => {
const db = useDb(); const db = useDb()
await db.teardown(); await db.teardown()
}); })
it("successfully deletes a product", async () => { it("successfully deletes a product", async () => {
const api = useApi(); const api = useApi()
const response = await api const response = await api
.delete("/admin/products/test-product", { .delete("/admin/products/test-product", {
@@ -291,21 +633,21 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data).toEqual( expect(response.data).toEqual(
expect.objectContaining({ expect.objectContaining({
id: "test-product", id: "test-product",
deleted: true, deleted: true,
}) })
); )
}); })
it("successfully creates product with soft-deleted product handle", async () => { it("successfully creates product with soft-deleted product handle", async () => {
const api = useApi(); const api = useApi()
// First we soft-delete the product // First we soft-delete the product
const response = await api const response = await api
@@ -315,11 +657,11 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.id).toEqual("test-product"); expect(response.data.id).toEqual("test-product")
// 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 = {
@@ -339,20 +681,20 @@ describe("/admin/products", () => {
options: [{ value: "large" }, { value: "green" }], options: [{ value: "large" }, { value: "green" }],
}, },
], ],
}; }
const res = await api.post("/admin/products", payload, { const res = await api.post("/admin/products", payload, {
headers: { headers: {
Authorization: "Bearer test_token", Authorization: "Bearer test_token",
}, },
}); })
expect(res.status).toEqual(200); expect(res.status).toEqual(200)
expect(res.data.product.handle).toEqual("test-product"); expect(res.data.product.handle).toEqual("test-product")
}); })
it("successfully deletes product collection", async () => { it("successfully deletes product collection", async () => {
const api = useApi(); const api = useApi()
// First we soft-delete the product collection // First we soft-delete the product collection
const response = await api const response = await api
@@ -362,15 +704,15 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.id).toEqual("test-collection"); expect(response.data.id).toEqual("test-collection")
}); })
it("successfully creates soft-deleted product collection", async () => { it("successfully creates soft-deleted product collection", async () => {
const api = useApi(); const api = useApi()
const response = await api const response = await api
.delete("/admin/collections/test-collection", { .delete("/admin/collections/test-collection", {
@@ -379,30 +721,40 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.id).toEqual("test-collection"); expect(response.data.id).toEqual("test-collection")
// Lets try to create a product collection with same handle as deleted one // Lets try to create a product collection with same handle as deleted one
const payload = { const payload = {
title: "Another test collection", title: "Another test collection",
handle: "test-collection", handle: "test-collection",
}; }
const res = await api.post("/admin/collections", payload, { const res = await api.post("/admin/collections", payload, {
headers: { headers: {
Authorization: "Bearer test_token", Authorization: "Bearer test_token",
}, },
}); })
expect(res.status).toEqual(200); expect(res.status).toEqual(200)
expect(res.data.collection.handle).toEqual("test-collection"); expect(res.data.collection.handle).toEqual("test-collection")
}); })
it("successfully creates soft-deleted product variant", async () => { it("successfully creates soft-deleted product variant", async () => {
const api = useApi(); const api = useApi()
const product = await api
.get("/admin/products/test-product", {
headers: {
Authorization: "bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
const response = await api const response = await api
.delete("/admin/products/test-product/variants/test-variant", { .delete("/admin/products/test-product/variants/test-variant", {
@@ -411,11 +763,11 @@ describe("/admin/products", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.variant_id).toEqual("test-variant"); expect(response.data.variant_id).toEqual("test-variant")
// Lets try to create a product collection with same handle as deleted one // Lets try to create a product collection with same handle as deleted one
const payload = { const payload = {
@@ -430,19 +782,18 @@ describe("/admin/products", () => {
amount: 100, amount: 100,
}, },
], ],
}; options: [{ option_id: "test-option", value: "inserted value" }],
}
const res = await api.post( const res = await api
"/admin/products/test-product/variants", .post("/admin/products/test-product/variants", payload, {
payload,
{
headers: { headers: {
Authorization: "Bearer test_token", Authorization: "Bearer test_token",
}, },
} })
); .catch((err) => console.log(err))
expect(res.status).toEqual(200); expect(res.status).toEqual(200)
expect(res.data.product.variants).toEqual( expect(res.data.product.variants).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -453,7 +804,7 @@ describe("/admin/products", () => {
barcode: "test-barcode", barcode: "test-barcode",
}), }),
]) ])
); )
}); })
}); })
}); })
+136 -20
View File
@@ -2,55 +2,56 @@ const {
ProductCollection, ProductCollection,
ProductTag, ProductTag,
ProductType, ProductType,
ProductOption,
Region, Region,
Product, Product,
ShippingProfile, ShippingProfile,
ProductVariant, ProductVariant,
Image, Image,
} = require("@medusajs/medusa"); } = require("@medusajs/medusa")
module.exports = async (connection, data = {}) => { module.exports = async (connection, data = {}) => {
const manager = connection.manager; const manager = connection.manager
const defaultProfile = await manager.findOne(ShippingProfile, { const defaultProfile = await manager.findOne(ShippingProfile, {
type: "default", type: "default",
}); })
const coll = manager.create(ProductCollection, { const coll = manager.create(ProductCollection, {
id: "test-collection", id: "test-collection",
handle: "test-collection", handle: "test-collection",
title: "Test collection", title: "Test collection",
}); })
await manager.save(coll); await manager.save(coll)
const tag = manager.create(ProductTag, { const tag = manager.create(ProductTag, {
id: "tag1", id: "tag1",
value: "123", value: "123",
}); })
await manager.save(tag); await manager.save(tag)
const type = manager.create(ProductType, { const type = manager.create(ProductType, {
id: "test-type", id: "test-type",
value: "test-type", value: "test-type",
}); })
await manager.save(type); await manager.save(type)
const image = manager.create(Image, { const image = manager.create(Image, {
id: "test-image", id: "test-image",
url: "test-image.png", url: "test-image.png",
}); })
await manager.save(image); await manager.save(image)
await manager.insert(Region, { await manager.insert(Region, {
id: "test-region", id: "test-region",
name: "Test Region", name: "Test Region",
currency_code: "usd", currency_code: "usd",
tax_rate: 0, tax_rate: 0,
}); })
const p = manager.create(Product, { const p = manager.create(Product, {
id: "test-product", id: "test-product",
@@ -64,23 +65,138 @@ module.exports = async (connection, data = {}) => {
{ id: "tag1", value: "123" }, { id: "tag1", value: "123" },
{ tag: "tag2", value: "456" }, { tag: "tag2", value: "456" },
], ],
options: [{ id: "test-option", title: "Default value" }], })
});
p.images = [image]; p.images = [image]
await manager.save(p); await manager.save(p)
await manager.insert(ProductVariant, { await manager.save(ProductOption, {
id: "test-option",
title: "test-option",
product_id: "test-product",
})
const variant1 = await manager.create(ProductVariant, {
id: "test-variant", id: "test-variant",
inventory_quantity: 10, inventory_quantity: 10,
title: "Test variant", title: "Test variant",
variant_rank: 0,
sku: "test-sku", sku: "test-sku",
ean: "test-ean", ean: "test-ean",
upc: "test-upc", upc: "test-upc",
barcode: "test-barcode", barcode: "test-barcode",
product_id: "test-product", product_id: "test-product",
prices: [{ id: "test-price", currency_code: "usd", amount: 100 }], prices: [{ id: "test-price", currency_code: "usd", amount: 100 }],
options: [{ id: "test-variant-option", value: "Default variant" }], options: [
}); {
}; id: "test-variant-option",
value: "Default variant",
option_id: "test-option",
},
],
})
await manager.save(variant1)
const variant2 = await manager.create(ProductVariant, {
id: "test-variant_1",
inventory_quantity: 10,
title: "Test variant rank (1)",
variant_rank: 2,
sku: "test-sku1",
ean: "test-ean1",
upc: "test-upc1",
barcode: "test-barcode 1",
product_id: "test-product",
prices: [{ id: "test-price1", currency_code: "usd", amount: 100 }],
options: [
{
id: "test-variant-option-1",
value: "Default variant 1",
option_id: "test-option",
},
],
})
await manager.save(variant2)
const variant3 = await manager.create(ProductVariant, {
id: "test-variant_2",
inventory_quantity: 10,
title: "Test variant rank (2)",
variant_rank: 1,
sku: "test-sku2",
ean: "test-ean2",
upc: "test-upc2",
product_id: "test-product",
prices: [{ id: "test-price2", currency_code: "usd", amount: 100 }],
options: [
{
id: "test-variant-option-2",
value: "Default variant 2",
option_id: "test-option",
},
],
})
await manager.save(variant3)
const p1 = manager.create(Product, {
id: "test-product1",
handle: "test-product1",
title: "Test product1",
profile_id: defaultProfile.id,
description: "test-product-description1",
collection_id: "test-collection",
type: { id: "test-type", value: "test-type" },
tags: [
{ id: "tag1", value: "123" },
{ tag: "tag2", value: "456" },
],
})
await manager.save(p1)
const variant4 = await manager.create(ProductVariant, {
id: "test-variant_3",
inventory_quantity: 10,
title: "Test variant rank (2)",
variant_rank: 1,
sku: "test-sku3",
ean: "test-ean3",
upc: "test-upc3",
product_id: "test-product1",
prices: [{ id: "test-price3", currency_code: "usd", amount: 100 }],
options: [
{
id: "test-variant-option-3",
value: "Default variant 3",
option_id: "test-option",
},
],
})
await manager.save(variant4)
const variant5 = await manager.create(ProductVariant, {
id: "test-variant_4",
inventory_quantity: 10,
title: "Test variant rank (2)",
variant_rank: 0,
sku: "test-sku4",
ean: "test-ean4",
upc: "test-upc4",
product_id: "test-product1",
prices: [{ id: "test-price4", currency_code: "usd", amount: 100 }],
options: [
{
id: "test-variant-option-4",
value: "Default variant 3",
option_id: "test-option",
},
],
})
await manager.save(variant5)
}
@@ -1,10 +1,80 @@
import { IdMap } from "medusa-test-utils" import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request" import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product" import { ProductServiceMock } from "../../../../../services/__mocks__/product"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile"
describe("POST /admin/products", () => { describe("POST /admin/products", () => {
describe("successful creation", () => { describe("successful creation with variants", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/products", {
payload: {
title: "Test Product with variants",
description: "Test Description",
tags: [{ id: "test", value: "test" }],
handle: "test-product",
options: [{ title: "Test" }],
variants: [
{
title: "Test",
prices: [
{
currency_code: "USD",
amount: 100,
},
],
options: [
{
value: "100",
},
],
},
],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
afterAll(async () => {
jest.clearAllMocks()
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("assigns invokes productVariantService with ranked variants", () => {
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
IdMap.getId("productWithOptions"),
{
title: "Test",
variant_rank: 0,
prices: [
{
currency_code: "USD",
amount: 100,
},
],
options: [
{
option_id: IdMap.getId("option1"),
value: "100",
},
],
inventory_quantity: 0,
}
)
})
})
describe("successful creation test", () => {
let subject let subject
beforeAll(async () => { beforeAll(async () => {
@@ -14,6 +84,7 @@ describe("POST /admin/products", () => {
description: "Test Description", description: "Test Description",
tags: [{ id: "test", value: "test" }], tags: [{ id: "test", value: "test" }],
handle: "test-product", handle: "test-product",
options: [{ title: "Denominations" }],
}, },
adminSession: { adminSession: {
jwt: { jwt: {
@@ -40,6 +111,7 @@ describe("POST /admin/products", () => {
tags: [{ id: "test", value: "test" }], tags: [{ id: "test", value: "test" }],
handle: "test-product", handle: "test-product",
is_giftcard: false, is_giftcard: false,
options: [{ title: "Denominations" }],
profile_id: IdMap.getId("default_shipping_profile"), profile_id: IdMap.getId("default_shipping_profile"),
}) })
}) })
@@ -328,6 +328,8 @@ export default async (req, res) => {
.create({ ...value, profile_id: shippingProfile.id }) .create({ ...value, profile_id: shippingProfile.id })
if (variants) { if (variants) {
for (const [i, variant] of variants.entries()) variant.variant_rank = i
const optionIds = value.options.map( const optionIds = value.options.map(
o => newProduct.options.find(newO => newO.title === o.title).id o => newProduct.options.find(newO => newO.title === o.title).id
) )
@@ -341,6 +343,7 @@ export default async (req, res) => {
option_id: optionIds[index], option_id: optionIds[index],
})), })),
} }
await productVariantService await productVariantService
.withTransaction(manager) .withTransaction(manager)
.create(newProduct.id, variant) .create(newProduct.id, variant)
@@ -0,0 +1,23 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class RankColumnWithDefaultValue1631104895519 implements MigrationInterface {
name = 'RankColumnWithDefaultValue1631104895519'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "product_variant" ADD "variant_rank" integer DEFAULT '0'`);
await queryRunner.query(`ALTER TABLE "product_option_value" DROP CONSTRAINT "FK_7234ed737ff4eb1b6ae6e6d7b01"`);
await queryRunner.query(`ALTER TABLE "product_option_value" ADD CONSTRAINT "FK_7234ed737ff4eb1b6ae6e6d7b01" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE cascade ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "money_amount" DROP CONSTRAINT "FK_17a06d728e4cfbc5bd2ddb70af0"`);
await queryRunner.query(`ALTER TABLE "money_amount" ADD CONSTRAINT "FK_17a06d728e4cfbc5bd2ddb70af0" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE cascade ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "product_variant" DROP COLUMN "variant_rank"`);
await queryRunner.query(`ALTER TABLE "product_option_value" DROP CONSTRAINT "FK_7234ed737ff4eb1b6ae6e6d7b01"`);
await queryRunner.query(`ALTER TABLE "product_option_value" ADD CONSTRAINT "FK_7234ed737ff4eb1b6ae6e6d7b01" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "money_amount" DROP CONSTRAINT "FK_17a06d728e4cfbc5bd2ddb70af0"`);
await queryRunner.query(`ALTER TABLE "money_amount" ADD CONSTRAINT "FK_17a06d728e4cfbc5bd2ddb70af0" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
}
+5 -1
View File
@@ -44,7 +44,11 @@ export class MoneyAmount {
@Column({ nullable: true }) @Column({ nullable: true })
variant_id: string variant_id: string
@ManyToOne(() => ProductVariant) @ManyToOne(
() => ProductVariant,
variant => variant.prices,
{ onDelete: "cascade" }
)
@JoinColumn({ name: "variant_id" }) @JoinColumn({ name: "variant_id" })
variant: ProductVariant variant: ProductVariant
@@ -41,7 +41,8 @@ export class ProductOptionValue {
@ManyToOne( @ManyToOne(
() => ProductVariant, () => ProductVariant,
variant => variant.options variant => variant.options,
{ onDelete: "cascade" }
) )
@JoinColumn({ name: "variant_id" }) @JoinColumn({ name: "variant_id" })
variant: ProductVariant variant: ProductVariant
@@ -43,7 +43,7 @@ export class ProductVariant {
@OneToMany( @OneToMany(
() => MoneyAmount, () => MoneyAmount,
ma => ma.variant, ma => ma.variant,
{ cascade: true } { cascade: true, onDelete: "CASCADE" }
) )
prices: MoneyAmount[] prices: MoneyAmount[]
@@ -63,6 +63,9 @@ export class ProductVariant {
@Index({ unique: true, where: "deleted_at IS NOT NULL" }) @Index({ unique: true, where: "deleted_at IS NOT NULL" })
upc: string upc: string
@Column({ nullable: true, default: 0, select:false })
variant_rank: number
@Column({ type: "int" }) @Column({ type: "int" })
inventory_quantity: number inventory_quantity: number
+26 -6
View File
@@ -19,7 +19,7 @@ export class ProductRepository extends Repository<Product> {
} }
const entitiesIds = entities.map(({ id }) => id) const entitiesIds = entities.map(({ id }) => id)
const groupedRelations = {} const groupedRelations : { [toplevel: string]: string[]} = {}
for (const rel of relations) { for (const rel of relations) {
const [topLevel] = rel.split(".") const [topLevel] = rel.split(".")
if (groupedRelations[topLevel]) { if (groupedRelations[topLevel]) {
@@ -30,13 +30,33 @@ export class ProductRepository extends Repository<Product> {
} }
const entitiesIdsWithRelations = await Promise.all( const entitiesIdsWithRelations = await Promise.all(
Object.entries(groupedRelations).map(([_, rels]) => { Object.entries(groupedRelations).map(([toplevel, rels]) => {
return this.findByIds(entitiesIds, { let querybuilder = this.createQueryBuilder("products")
select: ["id"],
relations: rels as string[], if (toplevel === "variants") {
}) querybuilder = querybuilder.leftJoinAndSelect(`products.${toplevel}`, toplevel, "variants.deleted_at IS NULL")
.orderBy({
"variants.variant_rank": "ASC",
})
} else {
querybuilder = querybuilder.leftJoinAndSelect(`products.${toplevel}`, toplevel)
}
for(const rel of rels) {
const [_, rest] = rel.split(".")
if (!rest) {
continue
}
// Regex matches all '.' except the rightmost
querybuilder = querybuilder.leftJoinAndSelect(rel.replace(/\.(?=[^.]*\.)/g,"__"), rel.replace(".", "__"))
}
return querybuilder
.where("products.deleted_at IS NULL AND products.id IN (:...entitiesIds)", { entitiesIds })
.getMany();
}) })
).then(flatten) ).then(flatten)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
@@ -36,7 +36,9 @@ export const ProductServiceMock = {
if (data.title === "Test Product") { if (data.title === "Test Product") {
return Promise.resolve(products.product1) return Promise.resolve(products.product1)
} }
if (data.title === "Test Product with variants") {
return Promise.resolve(products.productWithOptions)
}
return Promise.resolve({ ...data }) return Promise.resolve({ ...data })
}), }),
count: jest.fn().mockReturnValue(4), count: jest.fn().mockReturnValue(4),
@@ -148,6 +148,7 @@ describe("ProductVariantService", () => {
expect(productVariantRepository.create).toHaveBeenCalledWith({ expect(productVariantRepository.create).toHaveBeenCalledWith({
id: IdMap.getId("v2"), id: IdMap.getId("v2"),
product_id: IdMap.getId("ironman"), product_id: IdMap.getId("ironman"),
variant_rank: 1,
options: [ options: [
{ {
id: IdMap.getId("test"), id: IdMap.getId("test"),
@@ -11,9 +11,26 @@ const eventBusService = {
describe("ProductService", () => { describe("ProductService", () => {
describe("retrieve", () => { describe("retrieve", () => {
const productRepo = MockRepository({ const productRepo = MockRepository({
findOneWithRelations: () => findOneWithRelations: (rels, query) => {
Promise.resolve({ id: IdMap.getId("ironman") }), if (query.where.id === "test id with variants") {
return {
id: "test id with variants",
variants: [
{ id: "test_321", title: "Green" },
{ id: "test_123", title: "Blue" },
],
}
}
if (query.where.id === "test id one variant") {
return {
id: "test id one variant",
variants: [{ id: "test_123", title: "Blue" }],
}
}
return Promise.resolve({ id: IdMap.getId("ironman") })
},
}) })
const productService = new ProductService({ const productService = new ProductService({
manager: MockManager, manager: MockManager,
productRepository: productRepo, productRepository: productRepo,
@@ -37,11 +54,12 @@ describe("ProductService", () => {
describe("create", () => { describe("create", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
create: () => ({ create: product => ({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
title: "Suit", title: "Suit",
options: [], options: [],
collection: { id: IdMap.getId("cat"), title: "Suits" }, collection: { id: IdMap.getId("cat"), title: "Suits" },
variants: product.variants,
}), }),
findOneWithRelations: () => ({ findOneWithRelations: () => ({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
@@ -97,6 +115,16 @@ describe("ProductService", () => {
options: [], options: [],
tags: [{ value: "title" }, { value: "title2" }], tags: [{ value: "title" }, { value: "title2" }],
type: "type-1", type: "type-1",
variants: [
{
id: "test1",
title: "green",
},
{
id: "test2",
title: "blue",
},
],
}) })
expect(eventBusService.emit).toHaveBeenCalledTimes(1) expect(eventBusService.emit).toHaveBeenCalledTimes(1)
@@ -108,6 +136,16 @@ describe("ProductService", () => {
expect(productRepository.create).toHaveBeenCalledTimes(1) expect(productRepository.create).toHaveBeenCalledTimes(1)
expect(productRepository.create).toHaveBeenCalledWith({ expect(productRepository.create).toHaveBeenCalledWith({
title: "Suit", title: "Suit",
variants: [
{
id: "test1",
title: "green",
},
{
id: "test2",
title: "blue",
},
],
}) })
expect(productTagRepository.findOne).toHaveBeenCalledTimes(2) expect(productTagRepository.findOne).toHaveBeenCalledTimes(2)
@@ -124,14 +162,30 @@ describe("ProductService", () => {
title: "Suit", title: "Suit",
options: [], options: [],
tags: [ tags: [
{ id: "tag-1", value: "title" }, {
{ id: "tag-2", value: "title2" }, id: "tag-1",
value: "title",
},
{
id: "tag-2",
value: "title2",
},
], ],
type_id: "type", type_id: "type",
collection: { collection: {
id: IdMap.getId("cat"), id: IdMap.getId("cat"),
title: "Suits", title: "Suits",
}, },
variants: [
{
id: "test1",
title: "green",
},
{
id: "test2",
title: "blue",
},
],
}) })
}) })
}) })
@@ -148,6 +202,15 @@ describe("ProductService", () => {
if (query.where.id === "123") { if (query.where.id === "123") {
return undefined return undefined
} }
if (query.where.id === "ranking test") {
return Promise.resolve({
id: "ranking test",
variants: [
{ id: "test_321", title: "Greener", variant_rank: 1 },
{ id: "test_123", title: "Blueer", variant_rank: 0 },
],
})
}
return Promise.resolve({ id: IdMap.getId("ironman") }) return Promise.resolve({ id: IdMap.getId("ironman") })
}, },
}) })
@@ -165,7 +228,12 @@ describe("ProductService", () => {
withTransaction: function() { withTransaction: function() {
return this return this
}, },
update: () => Promise.resolve(), update: (variant, update) => {
if (variant.id) {
return update
}
return Promise.resolve()
},
} }
const productTagRepository = MockRepository({ const productTagRepository = MockRepository({
@@ -248,6 +316,30 @@ describe("ProductService", () => {
}) })
}) })
it("successfully updates variant ranking", async () => {
await productService.update("ranking test", {
variants: [
{ id: "test_321", title: "Greener", variant_rank: 1 },
{ id: "test_123", title: "Blueer", variant_rank: 0 },
],
})
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product.updated",
expect.any(Object)
)
expect(productRepository.save).toHaveBeenCalledTimes(1)
expect(productRepository.save).toHaveBeenCalledWith({
id: "ranking test",
variants: [
{ id: "test_321", title: "Greener", variant_rank: 0 },
{ id: "test_123", title: "Blueer", variant_rank: 1 },
],
})
})
it("successfully updates tags", async () => { it("successfully updates tags", async () => {
await productService.update(IdMap.getId("ironman"), { await productService.update(IdMap.getId("ironman"), {
tags: [ tags: [
@@ -174,6 +174,10 @@ class ProductVariantService extends BaseService {
) )
} }
if (!rest.variant_rank) {
rest.variant_rank = product.variants.length
}
const toCreate = { const toCreate = {
...rest, ...rest,
product_id: product.id, product_id: product.id,
+3 -1
View File
@@ -410,7 +410,9 @@ class ProductService extends BaseService {
} }
const newVariants = [] const newVariants = []
for (const newVariant of variants) { for (const [i, newVariant] of variants.entries()) {
newVariant.variant_rank = i
if (newVariant.id) { if (newVariant.id) {
const variant = product.variants.find(v => v.id === newVariant.id) const variant = product.variants.find(v => v.id === newVariant.id)