Feat(medusa) - delete cascade modules associations (#3190)

* delete cascade sales channel x locations, variant x inventory item
This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2023-02-08 17:23:47 -03:00
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
*/
+16 -1
View File
@@ -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