Feat(medusa) - delete cascade modules associations (#3190)
* delete cascade sales channel x locations, variant x inventory item
This commit is contained in:
1 parent
3b474ec35c
commit
d859ccf551
18 files changed
+670
-61
No files matched your search
@@ -362,5 +362,83 @@ describe("Inventory Items endpoints", () => {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("When deleting an inventory item it removes the product variants associated to it", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-product-new",
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
5
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/test-product-new/variants`,
|
||||||
|
{
|
||||||
|
title: "Test2",
|
||||||
|
sku: "MY_SKU2",
|
||||||
|
manage_inventory: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: "test-product-new-option",
|
||||||
|
value: "Blue",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: "Bearer test_token" } }
|
||||||
|
)
|
||||||
|
|
||||||
|
const secondVariantId = response.data.product.variants.find(
|
||||||
|
(v) => v.sku === "MY_SKU2"
|
||||||
|
).id
|
||||||
|
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
const variantInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const invItem2 = await inventoryService.createInventoryItem({
|
||||||
|
sku: "123456",
|
||||||
|
})
|
||||||
|
|
||||||
|
await variantInventoryService.attachInventoryItem(
|
||||||
|
variantId,
|
||||||
|
invItem2.id,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
await variantInventoryService.attachInventoryItem(
|
||||||
|
secondVariantId,
|
||||||
|
invItem2.id,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(variantId)
|
||||||
|
).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(
|
||||||
|
secondVariantId
|
||||||
|
)
|
||||||
|
).toHaveLength(2)
|
||||||
|
|
||||||
|
await api.delete(`/admin/inventory-items/${invItem2.id}`, {
|
||||||
|
headers: { Authorization: "Bearer test_token" },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(variantId)
|
||||||
|
).toHaveLength(1)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(
|
||||||
|
secondVariantId
|
||||||
|
)
|
||||||
|
).toHaveLength(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../../helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
const { simpleProductFactory } = require("../../../factories")
|
||||||
|
|
||||||
|
describe("Delete Variant", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Inventory Items", () => {
|
||||||
|
it("When deleting a product variant it removes the inventory items associated to it", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-product",
|
||||||
|
variants: [{ id: "test-variant" }],
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/test-product/variants`,
|
||||||
|
{
|
||||||
|
title: "Test Variant w. inventory",
|
||||||
|
sku: "MY_SKU",
|
||||||
|
manage_inventory: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: "test-product-option",
|
||||||
|
value: "SS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [{ currency_code: "usd", amount: 2300 }],
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: "Bearer test_token" } }
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantId = response.data.product.variants.find(
|
||||||
|
(v) => v.sku === "MY_SKU"
|
||||||
|
).id
|
||||||
|
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
const variantInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
const variantService = appContainer.resolve("productVariantService")
|
||||||
|
|
||||||
|
const invItem2 = await inventoryService.createInventoryItem({
|
||||||
|
sku: "123456",
|
||||||
|
})
|
||||||
|
|
||||||
|
await variantInventoryService.attachInventoryItem(
|
||||||
|
variantId,
|
||||||
|
invItem2.id,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(variantId)
|
||||||
|
).toHaveLength(2)
|
||||||
|
|
||||||
|
await api.delete(`/admin/products/test-product/variants/${variantId}`, {
|
||||||
|
headers: { Authorization: "Bearer test_token" },
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(variantService.retrieve(variantId)).rejects.toThrow(
|
||||||
|
`Variant with id: ${variantId} was not found`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(variantId)
|
||||||
|
).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
describe("Sales channels", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Stock Locations", () => {
|
||||||
|
it("When deleting a sales channel, removes all associated locations with it", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
const salesChannelService = appContainer.resolve("salesChannelService")
|
||||||
|
const salesChannelLocationService = appContainer.resolve(
|
||||||
|
"salesChannelLocationService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const loc = await stockLocationService.create({
|
||||||
|
name: "warehouse",
|
||||||
|
})
|
||||||
|
const loc2 = await stockLocationService.create({
|
||||||
|
name: "other place",
|
||||||
|
})
|
||||||
|
|
||||||
|
const sc = await salesChannelService.create({ name: "channel test" })
|
||||||
|
|
||||||
|
await salesChannelLocationService.associateLocation(sc.id, loc.id)
|
||||||
|
await salesChannelLocationService.associateLocation(sc.id, loc2.id)
|
||||||
|
|
||||||
|
expect(await salesChannelService.retrieve(sc.id)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: sc.id,
|
||||||
|
name: "channel test",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await salesChannelLocationService.listLocations(sc.id)
|
||||||
|
).toHaveLength(2)
|
||||||
|
|
||||||
|
await api.delete(`/admin/sales-channels/${sc.id}`, {
|
||||||
|
headers: { Authorization: "Bearer test_token" },
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(salesChannelService.retrieve(sc.id)).rejects.toThrowError()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
salesChannelLocationService.listLocations(sc.id)
|
||||||
|
).rejects.toThrowError()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
describe("Sales channels", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Stock Locations", () => {
|
||||||
|
it("When deleting a stock location, removes all associated sales channels with it", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
const salesChannelService = appContainer.resolve("salesChannelService")
|
||||||
|
const salesChannelLocationService = appContainer.resolve(
|
||||||
|
"salesChannelLocationService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const loc = await stockLocationService.create({
|
||||||
|
name: "warehouse",
|
||||||
|
})
|
||||||
|
|
||||||
|
const saleChannel = await salesChannelService.create({
|
||||||
|
name: "channel test",
|
||||||
|
})
|
||||||
|
|
||||||
|
const otherChannel = await salesChannelService.create({
|
||||||
|
name: "yet another channel",
|
||||||
|
})
|
||||||
|
|
||||||
|
await salesChannelLocationService.associateLocation(
|
||||||
|
saleChannel.id,
|
||||||
|
loc.id
|
||||||
|
)
|
||||||
|
await salesChannelLocationService.associateLocation(
|
||||||
|
otherChannel.id,
|
||||||
|
loc.id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await salesChannelLocationService.listLocations(saleChannel.id)
|
||||||
|
).toHaveLength(1)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await salesChannelLocationService.listLocations(otherChannel.id)
|
||||||
|
).toHaveLength(1)
|
||||||
|
|
||||||
|
await api.delete(`/admin/stock-locations/${loc.id}`, {
|
||||||
|
headers: { Authorization: "Bearer test_token" },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await salesChannelLocationService.listLocations(saleChannel.id)
|
||||||
|
).toHaveLength(0)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await salesChannelLocationService.listLocations(otherChannel.id)
|
||||||
|
).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
describe("Sales channels", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Stock Locations", () => {
|
||||||
|
it("When listing a sales channel, it brings all associated locations with it", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
const salesChannelService = appContainer.resolve("salesChannelService")
|
||||||
|
const salesChannelLocationService = appContainer.resolve(
|
||||||
|
"salesChannelLocationService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const loc = await stockLocationService.create({
|
||||||
|
name: "warehouse",
|
||||||
|
})
|
||||||
|
const loc2 = await stockLocationService.create({
|
||||||
|
name: "other place",
|
||||||
|
})
|
||||||
|
|
||||||
|
const sc = await salesChannelService.create({ name: "channel test" })
|
||||||
|
|
||||||
|
await salesChannelLocationService.associateLocation(sc.id, loc.id)
|
||||||
|
await salesChannelLocationService.associateLocation(sc.id, loc2.id)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await salesChannelLocationService.listLocations(sc.id)
|
||||||
|
).toHaveLength(2)
|
||||||
|
|
||||||
|
const [channels] = await salesChannelService.listAndCount(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
relations: ["locations"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const createdSC = channels.find((c) => c.id === sc.id)
|
||||||
|
|
||||||
|
expect(channels).toHaveLength(2)
|
||||||
|
expect(createdSC.locations).toHaveLength(2)
|
||||||
|
expect(createdSC).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: sc.id,
|
||||||
|
name: "channel test",
|
||||||
|
locations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
sales_channel_id: sc.id,
|
||||||
|
location_id: loc.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
sales_channel_id: sc.id,
|
||||||
|
location_id: loc2.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { IInventoryService } from "../../../../interfaces"
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
import { ProductVariantInventoryService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [delete] /inventory-items/{id}
|
* @oas [delete] /inventory-items/{id}
|
||||||
@@ -47,8 +48,15 @@ export default async (req: Request, res: Response) => {
|
|||||||
const inventoryService: IInventoryService =
|
const inventoryService: IInventoryService =
|
||||||
req.scope.resolve("inventoryService")
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
|
const productVariantInventoryService: ProductVariantInventoryService =
|
||||||
|
req.scope.resolve("productVariantInventoryService")
|
||||||
|
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
await manager.transaction(async (transactionManager) => {
|
await manager.transaction(async (transactionManager) => {
|
||||||
|
await productVariantInventoryService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.detachInventoryItem(id)
|
||||||
|
|
||||||
await inventoryService
|
await inventoryService
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.deleteInventoryItem(id)
|
.deleteInventoryItem(id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { IStockLocationService } from "../../../../interfaces"
|
import { IStockLocationService } from "../../../../interfaces"
|
||||||
|
import { SalesChannelLocationService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [delete] /stock-locations/{id}
|
* @oas [delete] /stock-locations/{id}
|
||||||
@@ -59,8 +60,15 @@ export default async (req, res) => {
|
|||||||
"stockLocationService"
|
"stockLocationService"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const salesChannelLocationService: SalesChannelLocationService =
|
||||||
|
req.scope.resolve("salesChannelLocationService")
|
||||||
|
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
await manager.transaction(async (transactionManager) => {
|
await manager.transaction(async (transactionManager) => {
|
||||||
|
await salesChannelLocationService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.removeLocation(id)
|
||||||
|
|
||||||
await stockLocationService.withTransaction(transactionManager).delete(id)
|
await stockLocationService.withTransaction(transactionManager).delete(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const inventoryService: IInventoryService =
|
const inventoryService: IInventoryService =
|
||||||
req.scope.resolve("inventoryService")
|
req.scope.resolve("inventoryService")
|
||||||
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
|
|
||||||
"salesChannelLocationService"
|
|
||||||
)
|
|
||||||
const channelService: SalesChannelService = req.scope.resolve(
|
const channelService: SalesChannelService = req.scope.resolve(
|
||||||
"salesChannelService"
|
"salesChannelService"
|
||||||
)
|
)
|
||||||
@@ -93,15 +91,11 @@ export default async (req, res) => {
|
|||||||
sales_channel_availability: [],
|
sales_channel_availability: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const [rawChannels] = await channelService.listAndCount({})
|
const [channels] = await channelService.listAndCount(
|
||||||
const channels: SalesChannelDTO[] = await Promise.all(
|
{},
|
||||||
rawChannels.map(async (channel) => {
|
{
|
||||||
const locations = await channelLocationService.listLocations(channel.id)
|
relations: ["locations"],
|
||||||
return {
|
|
||||||
...channel,
|
|
||||||
locations,
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const inventory =
|
const inventory =
|
||||||
@@ -122,7 +116,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const quantity = await inventoryService.retrieveAvailableQuantity(
|
const quantity = await inventoryService.retrieveAvailableQuantity(
|
||||||
inventory[0].id,
|
inventory[0].id,
|
||||||
channel.locations
|
channel.locations.map((loc) => loc.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -139,10 +133,6 @@ export default async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type SalesChannelDTO = Omit<SalesChannel, "beforeInsert"> & {
|
|
||||||
locations: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseInventoryItem = Partial<InventoryItemDTO> & {
|
type ResponseInventoryItem = Partial<InventoryItemDTO> & {
|
||||||
location_levels?: InventoryLevelDTO[]
|
location_levels?: InventoryLevelDTO[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
|
||||||
|
export class multiLocationSoftDelete1675689306130
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = "multiLocationSoftDelete1675689306130"
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE sales_channel_location
|
||||||
|
ADD COLUMN "deleted_at" TIMESTAMP WITH TIME ZONE;
|
||||||
|
|
||||||
|
DROP INDEX "IDX_6caaa358f12ed0b846f00e2dcd";
|
||||||
|
DROP INDEX "IDX_c2203162ca946a71aeb98390b0";
|
||||||
|
|
||||||
|
CREATE INDEX "IDX_sales_channel_location_sales_channel_id" ON "sales_channel_location" ("sales_channel_id") WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX "IDX_sales_channel_location_location_id" ON "sales_channel_location" ("location_id") WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE product_variant_inventory_item
|
||||||
|
ADD COLUMN "deleted_at" TIMESTAMP WITH TIME ZONE;
|
||||||
|
|
||||||
|
DROP INDEX "IDX_c74e8c2835094a37dead376a3b";
|
||||||
|
DROP INDEX "IDX_bf5386e7f2acc460adbf96d6f3";
|
||||||
|
|
||||||
|
CREATE INDEX "IDX_product_variant_inventory_item_inventory_item_id" ON "product_variant_inventory_item" ("inventory_item_id") WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX "IDX_product_variant_inventory_item_variant_id" ON "product_variant_inventory_item" ("variant_id") WHERE deleted_at IS NULL;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
DROP INDEX "IDX_sales_channel_location_sales_channel_id";
|
||||||
|
DROP INDEX "IDX_sales_channel_location_location_id";
|
||||||
|
|
||||||
|
DROP INDEX "IDX_product_variant_inventory_item_inventory_item_id";
|
||||||
|
DROP INDEX "IDX_product_variant_inventory_item_variant_id";
|
||||||
|
|
||||||
|
CREATE INDEX "IDX_6caaa358f12ed0b846f00e2dcd" ON "sales_channel_location" ("sales_channel_id");
|
||||||
|
CREATE INDEX "IDX_c2203162ca946a71aeb98390b0" ON "sales_channel_location" ("location_id");
|
||||||
|
|
||||||
|
CREATE INDEX "IDX_c74e8c2835094a37dead376a3b" ON "product_variant_inventory_item" ("inventory_item_id");
|
||||||
|
CREATE INDEX "IDX_bf5386e7f2acc460adbf96d6f3" ON "product_variant_inventory_item" ("variant_id");
|
||||||
|
|
||||||
|
ALTER TABLE sales_channel_location
|
||||||
|
DROP COLUMN "deleted_at";
|
||||||
|
|
||||||
|
ALTER TABLE product_variant_inventory_item
|
||||||
|
DROP COLUMN "deleted_at";
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,29 @@
|
|||||||
import { Index, Unique, BeforeInsert, Column, Entity } from "typeorm"
|
import {
|
||||||
import { BaseEntity } from "../interfaces/models/base-entity"
|
Index,
|
||||||
import { DbAwareColumn, generateEntityId } from "../utils"
|
BeforeInsert,
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from "typeorm"
|
||||||
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
|
import { generateEntityId } from "../utils"
|
||||||
|
import { ProductVariant } from "./product-variant"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(["variant_id", "inventory_item_id"])
|
export class ProductVariantInventoryItem extends SoftDeletableEntity {
|
||||||
export class ProductVariantInventoryItem extends BaseEntity {
|
|
||||||
@Index()
|
@Index()
|
||||||
@DbAwareColumn({ type: "text" })
|
@Column({ type: "text" })
|
||||||
inventory_item_id: string
|
inventory_item_id: string
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@DbAwareColumn({ type: "text" })
|
@Column({ type: "text" })
|
||||||
variant_id: string
|
variant_id: string
|
||||||
|
|
||||||
|
@ManyToOne(() => ProductVariant, (variant) => variant.inventory_items)
|
||||||
|
@JoinColumn({ name: "variant_id" })
|
||||||
|
variant: ProductVariant
|
||||||
|
|
||||||
@Column({ type: "int", default: 1 })
|
@Column({ type: "int", default: 1 })
|
||||||
required_quantity: number
|
required_quantity: number
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Product } from "./product"
|
|||||||
import { ProductOptionValue } from "./product-option-value"
|
import { ProductOptionValue } from "./product-option-value"
|
||||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
|
import { ProductVariantInventoryItem } from "./product-variant-inventory-item"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class ProductVariant extends SoftDeletableEntity {
|
export class ProductVariant extends SoftDeletableEntity {
|
||||||
@@ -91,6 +92,15 @@ export class ProductVariant extends SoftDeletableEntity {
|
|||||||
})
|
})
|
||||||
options: ProductOptionValue[]
|
options: ProductOptionValue[]
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => ProductVariantInventoryItem,
|
||||||
|
(inventoryItem) => inventoryItem.variant,
|
||||||
|
{
|
||||||
|
cascade: ["soft-remove", "remove"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
inventory_items: ProductVariantInventoryItem[]
|
||||||
|
|
||||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown>
|
metadata: Record<string, unknown>
|
||||||
|
|
||||||
@@ -199,6 +209,11 @@ export class ProductVariant extends SoftDeletableEntity {
|
|||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* $ref: "#/components/schemas/ProductOptionValue"
|
* $ref: "#/components/schemas/ProductOptionValue"
|
||||||
|
* inventory_items:
|
||||||
|
* description: The Inventory Items related to the product variant. Available if the relation `inventory_items` is expanded.
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: "#/components/schemas/ProductVariantInventoryItem"
|
||||||
* created_at:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The date with timezone at which the resource was created."
|
* description: "The date with timezone at which the resource was created."
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { BeforeInsert, Index, Column } from "typeorm"
|
import { BeforeInsert, Index, Column, ManyToOne, JoinColumn } from "typeorm"
|
||||||
|
|
||||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||||
import { BaseEntity } from "../interfaces"
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
import { generateEntityId } from "../utils"
|
import { generateEntityId } from "../utils"
|
||||||
|
import { SalesChannel } from "./sales-channel"
|
||||||
|
|
||||||
@FeatureFlagEntity("sales_channels")
|
@FeatureFlagEntity("sales_channels")
|
||||||
export class SalesChannelLocation extends BaseEntity {
|
export class SalesChannelLocation extends SoftDeletableEntity {
|
||||||
@Index()
|
@Index()
|
||||||
@Column({ type: "text" })
|
@Column({ type: "text" })
|
||||||
sales_channel_id: string
|
sales_channel_id: string
|
||||||
@@ -14,8 +15,42 @@ export class SalesChannelLocation extends BaseEntity {
|
|||||||
@Column({ type: "text" })
|
@Column({ type: "text" })
|
||||||
location_id: string
|
location_id: string
|
||||||
|
|
||||||
|
@ManyToOne(() => SalesChannel, (sc) => sc.locations)
|
||||||
|
@JoinColumn({ name: "sales_channel_id" })
|
||||||
|
sales_channel: SalesChannel
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): void {
|
private beforeInsert(): void {
|
||||||
this.id = generateEntityId(this.id, "scloc")
|
this.id = generateEntityId(this.id, "scloc")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema SalesChannelLocation
|
||||||
|
* title: "Sales Channel Stock Location"
|
||||||
|
* description: "Sales Channel Stock Location link sales channels with stock locations."
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* description: The Sales Channel Stock Location's ID
|
||||||
|
* example: scloc_01G8X9A7ESKAJXG2H0E6F1MW7A
|
||||||
|
* sales_channel_id:
|
||||||
|
* description: "The id of the Sales Channel"
|
||||||
|
* type: string
|
||||||
|
* location_id:
|
||||||
|
* description: "The id of the Location Stock."
|
||||||
|
* type: string
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* description: "The date with timezone at which the resource was created."
|
||||||
|
* format: date-time
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* description: "The date with timezone at which the resource was updated."
|
||||||
|
* format: date-time
|
||||||
|
* deleted_at:
|
||||||
|
* type: string
|
||||||
|
* description: "The date with timezone at which the resource was deleted."
|
||||||
|
* format: date-time
|
||||||
|
*/
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { BeforeInsert, Column } from "typeorm"
|
import { BeforeInsert, Column, OneToMany } from "typeorm"
|
||||||
|
|
||||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||||
import { SoftDeletableEntity } from "../interfaces"
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
import { generateEntityId } from "../utils"
|
import { generateEntityId } from "../utils"
|
||||||
|
import { SalesChannelLocation } from "./sales-channel-location"
|
||||||
|
|
||||||
@FeatureFlagEntity("sales_channels")
|
@FeatureFlagEntity("sales_channels")
|
||||||
export class SalesChannel extends SoftDeletableEntity {
|
export class SalesChannel extends SoftDeletableEntity {
|
||||||
@@ -15,6 +16,15 @@ export class SalesChannel extends SoftDeletableEntity {
|
|||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
is_disabled: boolean
|
is_disabled: boolean
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => SalesChannelLocation,
|
||||||
|
(scLocation) => scLocation.sales_channel,
|
||||||
|
{
|
||||||
|
cascade: ["soft-remove", "remove"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
locations: SalesChannelLocation[]
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
private beforeInsert(): void {
|
private beforeInsert(): void {
|
||||||
this.id = generateEntityId(this.id, "sc")
|
this.id = generateEntityId(this.id, "sc")
|
||||||
@@ -45,6 +55,11 @@ export class SalesChannel extends SoftDeletableEntity {
|
|||||||
* description: "Specify if the sales channel is enabled or disabled."
|
* description: "Specify if the sales channel is enabled or disabled."
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* default: false
|
* default: false
|
||||||
|
* locations:
|
||||||
|
* description: The Stock Locations related to the sales channel. Available if the relation `locations` is expanded.
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: "#/components/schemas/SalesChannelLocation"
|
||||||
* created_at:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* description: "The date with timezone at which the resource was created."
|
* description: "The date with timezone at which the resource was created."
|
||||||
|
|||||||
@@ -311,12 +311,12 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a variant from an inventory item
|
* Remove a variant from an inventory item
|
||||||
* @param variantId variant id
|
* @param variantId variant id or undefined if all the variants will be affected
|
||||||
* @param inventoryItemId inventory item id
|
* @param inventoryItemId inventory item id
|
||||||
*/
|
*/
|
||||||
async detachInventoryItem(
|
async detachInventoryItem(
|
||||||
variantId: string,
|
inventoryItemId: string,
|
||||||
inventoryItemId: string
|
variantId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const manager = this.transactionManager_ || this.manager_
|
const manager = this.transactionManager_ || this.manager_
|
||||||
|
|
||||||
@@ -324,15 +324,19 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
ProductVariantInventoryItem
|
ProductVariantInventoryItem
|
||||||
)
|
)
|
||||||
|
|
||||||
const existing = await variantInventoryRepo.findOne({
|
const where: any = {
|
||||||
where: {
|
|
||||||
variant_id: variantId,
|
|
||||||
inventory_item_id: inventoryItemId,
|
inventory_item_id: inventoryItemId,
|
||||||
},
|
}
|
||||||
|
if (variantId) {
|
||||||
|
where.variant_id = variantId
|
||||||
|
}
|
||||||
|
|
||||||
|
const varInvItems = await variantInventoryRepo.find({
|
||||||
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (existing) {
|
if (varInvItems.length) {
|
||||||
await variantInventoryRepo.remove(existing)
|
await variantInventoryRepo.remove(varInvItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -655,7 +655,7 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
|
|
||||||
const variant = await variantRepo.findOne({
|
const variant = await variantRepo.findOne({
|
||||||
where: { id: variantId },
|
where: { id: variantId },
|
||||||
relations: ["prices", "options"],
|
relations: ["prices", "options", "inventory_items"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
|
|||||||
@@ -33,19 +33,19 @@ class SalesChannelInventoryService {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the available quantity of an item across all sales channel locations
|
* Retrieves the available quantity of an item across all sales channel locations
|
||||||
* @param salesChannelId Sales channel id
|
* @param salesChannelId Sales channel id
|
||||||
* @param itemId Item id
|
* @param inventoryItemId Item id
|
||||||
* @returns available quantity of item across all sales channel locations
|
* @returns available quantity of item across all sales channel locations
|
||||||
*/
|
*/
|
||||||
async retrieveAvailableItemQuantity(
|
async retrieveAvailableItemQuantity(
|
||||||
salesChannelId: string,
|
salesChannelId: string,
|
||||||
itemId: string
|
inventoryItemId: string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const locations = await this.salesChannelLocationService_.listLocations(
|
const locations = await this.salesChannelLocationService_.listLocations(
|
||||||
salesChannelId
|
salesChannelId
|
||||||
)
|
)
|
||||||
|
|
||||||
return await this.inventoryService_.retrieveAvailableQuantity(
|
return await this.inventoryService_.retrieveAvailableQuantity(
|
||||||
itemId,
|
inventoryItemId,
|
||||||
locations
|
locations
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EntityManager } from "typeorm"
|
|||||||
import { IStockLocationService, TransactionBaseService } from "../interfaces"
|
import { IStockLocationService, TransactionBaseService } from "../interfaces"
|
||||||
import { SalesChannelService, EventBusService } from "./"
|
import { SalesChannelService, EventBusService } from "./"
|
||||||
|
|
||||||
import { SalesChannelLocation } from "../models"
|
import { SalesChannelLocation } from "../models/sales-channel-location"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
stockLocationService: IStockLocationService
|
stockLocationService: IStockLocationService
|
||||||
@@ -40,26 +40,39 @@ class SalesChannelLocationService extends TransactionBaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes an association between a sales channel and a stock location.
|
* Removes an association between a sales channel and a stock location.
|
||||||
* @param {string} salesChannelId - The ID of the sales channel.
|
* @param salesChannelId - The ID of the sales channel or undefined if all the sales channel will be affected.
|
||||||
* @param {string} locationId - The ID of the stock location.
|
* @param locationId - The ID of the stock location.
|
||||||
* @returns {Promise<void>} A promise that resolves when the association has been removed.
|
* @returns A promise that resolves when the association has been removed.
|
||||||
*/
|
*/
|
||||||
async removeLocation(
|
async removeLocation(
|
||||||
salesChannelId: string,
|
locationId: string,
|
||||||
locationId: string
|
salesChannelId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const manager = this.transactionManager_ || this.manager_
|
const manager = this.transactionManager_ || this.manager_
|
||||||
await manager.delete(SalesChannelLocation, {
|
|
||||||
sales_channel_id: salesChannelId,
|
const salesChannelLocationRepo = manager.getRepository(SalesChannelLocation)
|
||||||
|
|
||||||
|
const where: any = {
|
||||||
location_id: locationId,
|
location_id: locationId,
|
||||||
|
}
|
||||||
|
if (salesChannelId) {
|
||||||
|
where.sales_channel_id = salesChannelId
|
||||||
|
}
|
||||||
|
|
||||||
|
const scLoc = await salesChannelLocationRepo.find({
|
||||||
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (scLoc.length) {
|
||||||
|
await salesChannelLocationRepo.remove(scLoc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Associates a sales channel with a stock location.
|
* Associates a sales channel with a stock location.
|
||||||
* @param {string} salesChannelId - The ID of the sales channel.
|
* @param salesChannelId - The ID of the sales channel.
|
||||||
* @param {string} locationId - The ID of the stock location.
|
* @param locationId - The ID of the stock location.
|
||||||
* @returns {Promise<void>} A promise that resolves when the association has been created.
|
* @returns A promise that resolves when the association has been created.
|
||||||
*/
|
*/
|
||||||
async associateLocation(
|
async associateLocation(
|
||||||
salesChannelId: string,
|
salesChannelId: string,
|
||||||
@@ -70,16 +83,14 @@ class SalesChannelLocationService extends TransactionBaseService {
|
|||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.retrieve(salesChannelId)
|
.retrieve(salesChannelId)
|
||||||
|
|
||||||
const stockLocationId = locationId
|
|
||||||
|
|
||||||
if (this.stockLocationService) {
|
if (this.stockLocationService) {
|
||||||
const stockLocation = await this.stockLocationService.retrieve(locationId)
|
// trhows error if not found
|
||||||
locationId = stockLocation.id
|
await this.stockLocationService.retrieve(locationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const salesChannelLocation = manager.create(SalesChannelLocation, {
|
const salesChannelLocation = manager.create(SalesChannelLocation, {
|
||||||
sales_channel_id: salesChannel.id,
|
sales_channel_id: salesChannel.id,
|
||||||
location_id: stockLocationId,
|
location_id: locationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
await manager.save(salesChannelLocation)
|
await manager.save(salesChannelLocation)
|
||||||
@@ -87,8 +98,8 @@ class SalesChannelLocationService extends TransactionBaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the stock locations associated with a sales channel.
|
* Lists the stock locations associated with a sales channel.
|
||||||
* @param {string} salesChannelId - The ID of the sales channel.
|
* @param salesChannelId - The ID of the sales channel.
|
||||||
* @returns {Promise<string[]>} A promise that resolves with an array of location IDs.
|
* @returns A promise that resolves with an array of location IDs.
|
||||||
*/
|
*/
|
||||||
async listLocations(salesChannelId: string): Promise<string[]> {
|
async listLocations(salesChannelId: string): Promise<string[]> {
|
||||||
const manager = this.transactionManager_ || this.manager_
|
const manager = this.transactionManager_ || this.manager_
|
||||||
|
|||||||
@@ -230,9 +230,9 @@ class SalesChannelService extends TransactionBaseService {
|
|||||||
this.salesChannelRepository_
|
this.salesChannelRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const salesChannel = await this.retrieve(salesChannelId).catch(
|
const salesChannel = await this.retrieve(salesChannelId, {
|
||||||
() => void 0
|
relations: ["locations"],
|
||||||
)
|
}).catch(() => void 0)
|
||||||
|
|
||||||
if (!salesChannel) {
|
if (!salesChannel) {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in new issue
Block a user