feat(medusa, medusa-plugin-brightpearl): Inventory management for Brightpearl (#3192)
This commit is contained in:
7
.changeset/wet-teachers-compete.md
Normal file
7
.changeset/wet-teachers-compete.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"medusa-plugin-brightpearl": patch
|
||||
"@medusajs/inventory": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
feat(medusa-plugin-brightpearl, inventory, medusa): Multiwarehouse integration for brightpearl
|
||||
@@ -8,80 +8,6 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/orders/:id expands sales channel for single 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/orders?expand=sales_channels expands sales channel with parameter 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/sales-channels should list the sales channel using free text search 1`] = `
|
||||
Object {
|
||||
"count": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"sales_channels": ArrayContaining [
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description 2",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name 2",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/sales-channels should list the sales channel using properties filters 1`] = `
|
||||
Object {
|
||||
"count": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"sales_channels": ArrayContaining [
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels GET /admin/sales-channels/:id should retrieve the requested sales channel 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels POST /admin/sales-channels successfully creates a disabled sales channel 1`] = `
|
||||
Object {
|
||||
"sales_channel": ObjectContaining {
|
||||
@@ -100,15 +26,3 @@ Object {
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels POST /admin/sales-channels/:id updates sales channel properties 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "updated description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": true,
|
||||
"name": "updated name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -71,13 +71,15 @@ describe("sales channels", () => {
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.sales_channel).toBeTruthy()
|
||||
expect(response.data.sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: salesChannel.name,
|
||||
description: salesChannel.description,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
expect(response.data.sales_channel).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: salesChannel.name,
|
||||
description: salesChannel.description,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -135,12 +137,12 @@ describe("sales channels", () => {
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.sales_channels).toBeTruthy()
|
||||
expect(response.data.sales_channels.length).toBe(1)
|
||||
expect(response.data).toMatchSnapshot({
|
||||
expect(response.data).toEqual({
|
||||
count: 1,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
sales_channels: expect.arrayContaining([
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: salesChannel2.name,
|
||||
description: salesChannel2.description,
|
||||
@@ -148,7 +150,7 @@ describe("sales channels", () => {
|
||||
deleted_at: null,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
@@ -163,12 +165,12 @@ describe("sales channels", () => {
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.sales_channels).toBeTruthy()
|
||||
expect(response.data.sales_channels.length).toBe(1)
|
||||
expect(response.data).toMatchSnapshot({
|
||||
expect(response.data).toEqual({
|
||||
count: 1,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
sales_channels: expect.arrayContaining([
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: salesChannel1.name,
|
||||
description: salesChannel1.description,
|
||||
@@ -176,7 +178,7 @@ describe("sales channels", () => {
|
||||
deleted_at: null,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
@@ -218,14 +220,16 @@ describe("sales channels", () => {
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
is_disabled: payload.is_disabled,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
expect(response.data.sales_channel).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: payload.name,
|
||||
description: payload.description,
|
||||
is_disabled: payload.is_disabled,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -445,14 +449,16 @@ describe("sales channels", () => {
|
||||
)
|
||||
|
||||
expect(response.data.order.sales_channel).toBeTruthy()
|
||||
expect(response.data.order.sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
expect(response.data.order.sales_channel).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -482,14 +488,16 @@ describe("sales channels", () => {
|
||||
)
|
||||
|
||||
expect(response.data.orders[0].sales_channel).toBeTruthy()
|
||||
expect(response.data.orders[0].sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
expect(response.data.orders[0].sales_channel).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -712,15 +720,17 @@ describe("sales channels", () => {
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.sales_channel).toEqual({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
})
|
||||
expect(response.data.sales_channel).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
deleted_at: null,
|
||||
})
|
||||
)
|
||||
|
||||
const attachedProduct = await dbConnection.manager.findOne(Product, {
|
||||
where: { id: product.id },
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`sales channels GET /store/cart/:id returns cart with sales channel for single cart 1`] = `
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
}
|
||||
`;
|
||||
@@ -281,14 +281,16 @@ describe("sales channels", () => {
|
||||
const response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
||||
|
||||
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||
expect(response.data.cart.sales_channel).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
expect(response.data.cart.sales_channel).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
is_disabled: false,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as setup from "./schema-migrations/1665748086258-inventory_setup"
|
||||
import * as addExternalId from "./schema-migrations/1675761451145-add_reservation_external_id"
|
||||
|
||||
export default [setup]
|
||||
export default [setup, addExternalId]
|
||||
|
||||
@@ -26,7 +26,6 @@ export class inventorySetup1665748086258 implements MigrationInterface {
|
||||
|
||||
CREATE UNIQUE INDEX "IDX_inventory_item_sku" ON "inventory_item" ("sku") WHERE deleted_at IS NULL;
|
||||
|
||||
|
||||
CREATE TABLE "reservation_item" (
|
||||
"id" character varying NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class addReservationType1675761451145 implements MigrationInterface {
|
||||
name = "addReservationType1675761451145"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "reservation_item" ADD "external_id" character varying
|
||||
`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "reservation_item" DROP COLUMN "external_id";
|
||||
`)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./reservation-item"
|
||||
export {ReservationItem} from "./reservation-item"
|
||||
export * from "./inventory-item"
|
||||
export * from "./inventory-level"
|
||||
|
||||
@@ -18,6 +18,9 @@ export class ReservationItem extends SoftDeletableEntity {
|
||||
@Column()
|
||||
quantity: number
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
external_id: string | null
|
||||
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown> | null
|
||||
|
||||
|
||||
@@ -132,18 +132,19 @@ export default class ReservationItemService {
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<ReservationItem> {
|
||||
const manager = context.transactionManager!
|
||||
const itemRepository = manager.getRepository(ReservationItem)
|
||||
const reservationItemRepository = manager.getRepository(ReservationItem)
|
||||
|
||||
const inventoryItem = itemRepository.create({
|
||||
const reservationItem = reservationItemRepository.create({
|
||||
inventory_item_id: data.inventory_item_id,
|
||||
line_item_id: data.line_item_id,
|
||||
location_id: data.location_id,
|
||||
quantity: data.quantity,
|
||||
metadata: data.metadata,
|
||||
external_id: data.external_id,
|
||||
})
|
||||
|
||||
const [newInventoryItem] = await Promise.all([
|
||||
itemRepository.save(inventoryItem),
|
||||
const [newReservationItem] = await Promise.all([
|
||||
reservationItemRepository.save(reservationItem),
|
||||
this.inventoryLevelService_.adjustReservedQuantity(
|
||||
data.inventory_item_id,
|
||||
data.location_id,
|
||||
@@ -153,10 +154,10 @@ export default class ReservationItemService {
|
||||
])
|
||||
|
||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.CREATED, {
|
||||
id: newInventoryItem.id,
|
||||
id: newReservationItem.id,
|
||||
})
|
||||
|
||||
return newInventoryItem
|
||||
return newReservationItem
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { MedusaError, humanizeAmount } from "medusa-core-utils"
|
||||
import {
|
||||
ReservationType,
|
||||
updateInventoryAndReservations,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import Brightpearl from "../utils/brightpearl"
|
||||
|
||||
@@ -14,6 +19,13 @@ class BrightpearlService extends BaseService {
|
||||
swapService,
|
||||
claimService,
|
||||
discountService,
|
||||
stockLocationService,
|
||||
inventoryService,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
productVariantInventoryService,
|
||||
salesChannelLocationService,
|
||||
logger,
|
||||
},
|
||||
options
|
||||
) {
|
||||
@@ -22,6 +34,7 @@ class BrightpearlService extends BaseService {
|
||||
this.manager_ = manager
|
||||
this.options = options
|
||||
this.productVariantService_ = productVariantService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.regionService_ = regionService
|
||||
this.orderService_ = orderService
|
||||
this.totalsService_ = totalsService
|
||||
@@ -29,6 +42,12 @@ class BrightpearlService extends BaseService {
|
||||
this.oauthService_ = oauthService
|
||||
this.swapService_ = swapService
|
||||
this.claimService_ = claimService
|
||||
this.stockLocationService_ = stockLocationService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.lineItemService_ = lineItemService
|
||||
this.eventBusService_ = eventBusService
|
||||
this.salesChannelLocationService_ = salesChannelLocationService
|
||||
this.logger_ = logger
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
@@ -85,13 +104,14 @@ class BrightpearlService extends BaseService {
|
||||
|
||||
async verifyWebhooks() {
|
||||
const brightpearl = await this.getClient()
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
subscribeTo: "goods-out-note.created",
|
||||
httpMethod: "POST",
|
||||
uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`,
|
||||
bodyTemplate:
|
||||
'{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }',
|
||||
"{\"account\": \"${account-code}\", \"lifecycle_event\": \"${lifecycle-event}\", \"resource_type\": \"${resource-type}\", \"id\": \"${resource-id}\" }",
|
||||
contentType: "application/json",
|
||||
idSetAccepted: false,
|
||||
},
|
||||
@@ -166,35 +186,216 @@ class BrightpearlService extends BaseService {
|
||||
availabilities = Object.assign(availabilities, chunkAvails)
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
bpProducts.map(async (bpProduct) => {
|
||||
const { SKU: sku, productId } = bpProduct
|
||||
|
||||
const variant = await this.productVariantService_
|
||||
.retrieveBySKU(sku, {
|
||||
select: ["id", "manage_inventory", "inventory_quantity"],
|
||||
if (!this.inventoryService_) {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const [variants] = await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.listAndCount({
|
||||
sku: bpProducts.map(({ SKU }) => SKU),
|
||||
})
|
||||
.catch((_) => undefined)
|
||||
|
||||
const prodAvail = availabilities[productId]
|
||||
const variantsMap = new Map(
|
||||
variants.filter((variant) => !!variant.sku).map((v) => [v.sku, v])
|
||||
)
|
||||
|
||||
let onHand = 0
|
||||
if (
|
||||
prodAvail &&
|
||||
prodAvail.warehouses &&
|
||||
prodAvail.warehouses[`${this.options.warehouse}`]
|
||||
) {
|
||||
onHand = prodAvail.warehouses[`${this.options.warehouse}`].onHand
|
||||
}
|
||||
const variantUpdates = await Promise.all(
|
||||
bpProducts.map(async (bpProduct) => {
|
||||
const { SKU: sku, productId } = bpProduct
|
||||
|
||||
if (variant && variant.manage_inventory) {
|
||||
if (parseInt(variant.inventory_quantity) !== parseInt(onHand)) {
|
||||
return this.productVariantService_.update(variant.id, {
|
||||
inventory_quantity: parseInt(onHand),
|
||||
})
|
||||
}
|
||||
}
|
||||
const variant = variantsMap.get(sku)
|
||||
|
||||
const productAvailability = availabilities[productId]
|
||||
|
||||
let onHand = 0
|
||||
if (
|
||||
productAvailability &&
|
||||
productAvailability.warehouses &&
|
||||
productAvailability.warehouses[`${this.options.warehouse}`]
|
||||
) {
|
||||
onHand =
|
||||
productAvailability.warehouses[`${this.options.warehouse}`]
|
||||
.onHand
|
||||
}
|
||||
|
||||
if (variant && variant.manage_inventory) {
|
||||
if (parseInt(variant.inventory_quantity) !== parseInt(onHand)) {
|
||||
return {
|
||||
variant,
|
||||
update: { inventory_quantity: parseInt(onHand) },
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
return this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.update(variantUpdates.filter(Boolean))
|
||||
})
|
||||
} else {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const [inventoryItems, count] =
|
||||
await this.inventoryService_.listInventoryItems(
|
||||
{
|
||||
sku: bpProducts.map(({ SKU }) => SKU),
|
||||
},
|
||||
{},
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
|
||||
const itemMap = new Map(inventoryItems.map((i) => [i.id, i.sku]))
|
||||
|
||||
const [inventoryLevels, levelsCount] =
|
||||
await this.inventoryService_.listInventoryLevels(
|
||||
{
|
||||
inventory_item_id: inventoryItems.map((i) => i.id),
|
||||
},
|
||||
{},
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
|
||||
const locations = (
|
||||
await this.stockLocationService_.list(
|
||||
{
|
||||
id: [...new Set(inventoryLevels.map((ri) => ri.location_id))],
|
||||
},
|
||||
{},
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
).filter((location) => !!location.metadata?.bp_id)
|
||||
|
||||
const inventoryMap = inventoryLevels.reduce((acc, level) => {
|
||||
const itemSku = itemMap.get(level.inventory_item_id)
|
||||
if (!itemSku) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const locationsMap = acc.get(itemSku)
|
||||
if (!locationsMap) {
|
||||
acc.set(itemSku, new Map([[level.location_id, level]]))
|
||||
} else {
|
||||
locationsMap.set(level.location_id, level)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, new Map())
|
||||
|
||||
this.logger_.info("Synchronizing inventory levels")
|
||||
|
||||
await Promise.all(
|
||||
bpProducts.map(async (bpProduct, index) => {
|
||||
if (index % 100 === 0) {
|
||||
this.logger_.info(
|
||||
`Synchronizing ${index} of ${bpProducts.length}`
|
||||
)
|
||||
}
|
||||
|
||||
const { SKU: sku, productId } = bpProduct
|
||||
|
||||
const productAvailability = availabilities[productId]
|
||||
|
||||
if (productAvailability) {
|
||||
await Promise.all(
|
||||
locations.map(async (location) => {
|
||||
const warehouseData =
|
||||
productAvailability.warehouses[
|
||||
`${location.metadata.bp_id}`
|
||||
]
|
||||
|
||||
const inventoryLevel = inventoryMap
|
||||
.get(sku)
|
||||
?.get(location.id)
|
||||
|
||||
if (!inventoryLevel || !warehouseData) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.adjustMedusaLocationLevel_(
|
||||
location,
|
||||
inventoryLevel,
|
||||
warehouseData
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
this.logger_.info("Finished synchronizing inventory levels")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async adjustCoreInventory_(variantId, productAvailability) {
|
||||
let onHand = 0
|
||||
|
||||
if (
|
||||
productAvailability.warehouses &&
|
||||
productAvailability.warehouses[`${this.options.warehouse}`]
|
||||
) {
|
||||
onHand =
|
||||
productAvailability.warehouses[`${this.options.warehouse}`].onHand
|
||||
}
|
||||
|
||||
return await this.manager_.transaction((m) => {
|
||||
return this.productVariantService_.withTransaction(m).update(variantId, {
|
||||
inventory_quantity: onHand,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async adjustMedusaLocationLevel_(location, inventoryLevel, warehouseData) {
|
||||
const manager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
if (inventoryLevel.stocked_quantity !== warehouseData.inStock) {
|
||||
await this.inventoryService_.updateInventoryLevel(
|
||||
inventoryLevel.inventory_item_id,
|
||||
inventoryLevel.location_id,
|
||||
{ stocked_quantity: warehouseData.inStock },
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
}
|
||||
|
||||
const externallyReservedQuantityAdjustment =
|
||||
warehouseData.inStock -
|
||||
warehouseData.onHand -
|
||||
inventoryLevel.reserved_quantity
|
||||
|
||||
if (externallyReservedQuantityAdjustment === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const [reservations] = await this.inventoryService_.listReservationItems(
|
||||
{
|
||||
inventory_item_id: inventoryLevel.inventory_item_id,
|
||||
location_id: location.id,
|
||||
external_id: "brightpearl",
|
||||
},
|
||||
{},
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
|
||||
const externalReservation = reservations.find(
|
||||
(r) => r.external_id === "brightpearl"
|
||||
)
|
||||
|
||||
if (externalReservation) {
|
||||
await this.inventoryService_.updateReservationItem(
|
||||
externalReservation.id,
|
||||
{
|
||||
quantity:
|
||||
externalReservation.quantity + externallyReservedQuantityAdjustment,
|
||||
},
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
} else {
|
||||
await this.inventoryService_.createReservationItem(
|
||||
{
|
||||
location_id: location.id,
|
||||
inventory_item_id: inventoryLevel.inventory_item_id,
|
||||
external_id: "brightpearl",
|
||||
quantity: externallyReservedQuantityAdjustment,
|
||||
},
|
||||
{ transactionManager: manager }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -205,35 +406,75 @@ class BrightpearlService extends BaseService {
|
||||
.retrieveAvailability(productId)
|
||||
.catch(() => null)
|
||||
|
||||
if (availability) {
|
||||
const brightpearlProduct = await client.products.retrieve(productId)
|
||||
if (!availability) {
|
||||
return
|
||||
}
|
||||
|
||||
const prodAvail = availability[productId]
|
||||
const brightpearlProduct = await client.products.retrieve(productId)
|
||||
|
||||
let onHand = 0
|
||||
if (
|
||||
prodAvail.warehouses &&
|
||||
prodAvail.warehouses[`${this.options.warehouse}`]
|
||||
) {
|
||||
onHand = prodAvail.warehouses[`${this.options.warehouse}`].onHand
|
||||
}
|
||||
const sku = brightpearlProduct.identity.sku
|
||||
if (!sku) {
|
||||
return
|
||||
}
|
||||
|
||||
const sku = brightpearlProduct.identity.sku
|
||||
if (!sku) return
|
||||
const productAvailability = availability[productId]
|
||||
|
||||
if (!this.inventoryService_) {
|
||||
const variant = await this.productVariantService_
|
||||
.retrieveBySKU(sku)
|
||||
.catch((_) => undefined)
|
||||
if (variant && variant.manage_inventory) {
|
||||
await this.manager_.transaction((m) => {
|
||||
return this.productVariantService_
|
||||
.withTransaction(m)
|
||||
.update(variant.id, {
|
||||
inventory_quantity: onHand,
|
||||
})
|
||||
})
|
||||
|
||||
if (!variant?.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.adjustCoreInventory_(variant.id, productAvailability)
|
||||
}
|
||||
|
||||
const [inventoryItems] = await this.inventoryService_.listInventoryItems({
|
||||
sku,
|
||||
})
|
||||
|
||||
const [inventoryLevels] = await this.inventoryService_.listInventoryLevels({
|
||||
inventory_item_id: inventoryItems.map((i) => i.id),
|
||||
})
|
||||
|
||||
const inventoryMap = inventoryLevels.reduce((acc, item) => {
|
||||
acc[item.location_id] = acc[item.location_id]
|
||||
? [...acc[item.location_id], item]
|
||||
: [item]
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const locations = (
|
||||
await this.stockLocationService_.list({
|
||||
id: inventoryLevels.map((ri) => ri.location_id),
|
||||
})
|
||||
).filter(
|
||||
(location) =>
|
||||
location.metadata?.bp_id &&
|
||||
productAvailability.warehouses[`${location.metadata.bp_id}`]
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
locations.map(async (location) => {
|
||||
// TODO: Assuming we have a 1 to 1 mapping of inventory items
|
||||
const inventoryLevel = inventoryMap[location.id][0]
|
||||
|
||||
const warehouseData =
|
||||
productAvailability.warehouses[`${location.metadata.bp_id}`]
|
||||
|
||||
if (!warehouseData) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.adjustMedusaLocationLevel_(
|
||||
location,
|
||||
inventoryLevel,
|
||||
warehouseData
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async createGoodsOutNote(fromOrder, shipment) {
|
||||
@@ -363,6 +604,276 @@ class BrightpearlService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async getBrightPearlWarehouseFromMedusaLocation_(locationId) {
|
||||
let warehouse = this.options.warehouse
|
||||
|
||||
if (locationId && this.stockLocationService_) {
|
||||
const location = await this.stockLocationService_.retrieve(locationId)
|
||||
if (location?.metadata?.bp_id) {
|
||||
warehouse = location.metadata.bp_id
|
||||
}
|
||||
}
|
||||
|
||||
return warehouse
|
||||
}
|
||||
|
||||
async getOrderFromReservation_(reservationItems) {
|
||||
if (!reservationItems.some((item) => !!item.line_item_id)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const lineItems = await this.lineItemService_.list(
|
||||
{
|
||||
id: reservationItems
|
||||
.filter((item) => !!item.line_item_id)
|
||||
.map((item) => item.line_item_id),
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
if (!lineItems.length || !lineItems[0].order_id) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const order = await this.orderService_
|
||||
.retrieve(lineItems[0].order_id)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (order) {
|
||||
return { order, lineItems }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
async bulkCreateReservation(eventData) {
|
||||
const { ids } = eventData
|
||||
|
||||
if (!ids.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const [reservationItems] =
|
||||
await this.inventoryService_.listReservationItems({
|
||||
id: ids,
|
||||
})
|
||||
|
||||
const client = await this.getClient()
|
||||
|
||||
const { order, lineItems } = await this.getOrderFromReservation_(
|
||||
reservationItems
|
||||
)
|
||||
|
||||
if (!order?.metadata?.brightpearl_sales_order_id || !lineItems?.length) {
|
||||
return this.attemptRetryEvent(
|
||||
"reservation-items.bulk-created",
|
||||
eventData,
|
||||
"Cannot create a brightpearl reservation without a brightpearl order"
|
||||
)
|
||||
}
|
||||
|
||||
const warehouse = await this.getBrightPearlWarehouseFromMedusaLocation_(
|
||||
reservationItems[0].location_id
|
||||
)
|
||||
|
||||
const variants = await this.productVariantService_.list(
|
||||
{ id: lineItems.map((item) => item.variant_id) },
|
||||
{}
|
||||
)
|
||||
const lineItemMap = new Map(lineItems.map((item) => [item.id, item]))
|
||||
const variantMap = new Map(variants.map((v) => [v.id, v]))
|
||||
|
||||
const bpOrder = await client.orders.retrieve(
|
||||
order.metadata.brightpearl_sales_order_id
|
||||
)
|
||||
|
||||
const rows = await Promise.all(
|
||||
reservationItems.map(async (item) => {
|
||||
const lineItem = lineItemMap.get(item.line_item_id)
|
||||
const variant = variantMap.get(lineItem.variant_id)
|
||||
|
||||
const bpProduct = await this.retrieveProductBySKU(variant.sku)
|
||||
|
||||
if (!lineItem || !variant || !bpProduct) {
|
||||
return null
|
||||
}
|
||||
|
||||
const bpOrderRow = bpOrder.rows.find(
|
||||
(row) => row.externalRef === lineItem.id
|
||||
)
|
||||
|
||||
return {
|
||||
productId: bpProduct.productId,
|
||||
id: bpOrderRow.id,
|
||||
quantity: item.quantity,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
order.rows = rows.filter((row) => !!row)
|
||||
|
||||
const reservation = await client.warehouses
|
||||
.retrieveReservation(order.metadata.brightpearl_sales_order_id)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!reservation) {
|
||||
const reservationFailed = await client.warehouses
|
||||
.createReservation(
|
||||
{ ...order, id: order.metadata.brightpearl_sales_order_id },
|
||||
warehouse
|
||||
)
|
||||
.catch(() => true)
|
||||
|
||||
// if we succeed in creating the reservation return early
|
||||
if (!reservationFailed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!reservation) {
|
||||
return this.attemptRetryEvent(
|
||||
"reservation-items.bulk-created",
|
||||
eventData,
|
||||
"Could not create reservation for order with id: " +
|
||||
order.metadata.brightpearl_sales_order_id
|
||||
)
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
products: [
|
||||
...order.rows.map((row) => ({
|
||||
productId: row.productId,
|
||||
salesOrderRowId: row.id,
|
||||
quantity: row.quantity,
|
||||
})),
|
||||
...Object.entries(reservation[0].orderRows).map(([key, value]) => ({
|
||||
productId: value.productId,
|
||||
quantity: value.quantity,
|
||||
salesOrderRowId: key,
|
||||
})),
|
||||
],
|
||||
}
|
||||
|
||||
return await client.warehouses.updateReservation(
|
||||
order.metadata.brightpearl_sales_order_id,
|
||||
updatePayload
|
||||
)
|
||||
}
|
||||
|
||||
async createReservation(eventData) {
|
||||
const { id } = eventData
|
||||
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
const [[reservationItem]] =
|
||||
await this.inventoryService_.listReservationItems({
|
||||
id,
|
||||
})
|
||||
|
||||
const client = await this.getClient()
|
||||
|
||||
const { order, lineItems } = await this.getOrderFromReservation_([
|
||||
reservationItem,
|
||||
])
|
||||
|
||||
if (!order.metadata?.brightpearl_sales_order_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot create a brightpearl reservation without a brightpearl order"
|
||||
)
|
||||
}
|
||||
|
||||
const warehouse = await this.getBrightPearlWarehouseFromMedusaLocation_(
|
||||
reservationItem.location_id
|
||||
)
|
||||
|
||||
const variant = await this.productVariantService_.retrieve(
|
||||
lineItems[0].variant_id
|
||||
)
|
||||
|
||||
const bpProduct = await this.retrieveProductBySKU(variant.sku)
|
||||
|
||||
const bpOrder = await client.orders.retrieve(
|
||||
order.metadata.brightpearl_sales_order_id
|
||||
)
|
||||
|
||||
const bpOrderRow = bpOrder.rows.find(
|
||||
(row) => row.externalRef === lineItems[0].id
|
||||
)
|
||||
|
||||
order.rows = [
|
||||
{
|
||||
productId: bpProduct.productId,
|
||||
id: bpOrderRow.id,
|
||||
quantity: reservationItem.quantity,
|
||||
},
|
||||
]
|
||||
|
||||
const reservation = await client.warehouses
|
||||
.retrieveReservation(order.metadata.brightpearl_sales_order_id)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!reservation) {
|
||||
const reservationFailed = await client.warehouses
|
||||
.createReservation(
|
||||
{ ...order, id: order.metadata.brightpearl_sales_order_id },
|
||||
warehouse
|
||||
)
|
||||
.catch(() => true)
|
||||
|
||||
// if we succeed in creating the reservation return early
|
||||
if (!reservationFailed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!reservation) {
|
||||
return this.attemptRetryEvent(
|
||||
"product_variant_inventory.reservation_created",
|
||||
eventData,
|
||||
"Could not create reservation for order with id: " +
|
||||
order.metadata.brightpearl_sales_order_id
|
||||
)
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
products: [
|
||||
{
|
||||
productId: bpProduct.productId,
|
||||
salesOrderRowId: bpOrderRow.id,
|
||||
quantity: reservationItem.quantity,
|
||||
},
|
||||
...Object.entries(reservation[0].orderRows).map(([key, value]) => ({
|
||||
productId: value.productId,
|
||||
quantity: value.quantity,
|
||||
salesOrderRowId: key,
|
||||
})),
|
||||
],
|
||||
}
|
||||
|
||||
return await client.warehouses.updateReservation(
|
||||
order.metadata.brightpearl_sales_order_id,
|
||||
updatePayload
|
||||
)
|
||||
}
|
||||
|
||||
attemptRetryEvent(eventName, eventData, errorMessage) {
|
||||
const currentAttempts = eventData.retries || 0
|
||||
|
||||
if (currentAttempts > 3) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, errorMessage)
|
||||
}
|
||||
|
||||
const event = {
|
||||
...eventData,
|
||||
retries: 1 + currentAttempts,
|
||||
}
|
||||
|
||||
this.eventBusService_.emit(eventName, event)
|
||||
}
|
||||
|
||||
async createSalesCredit(fromOrder, fromReturn) {
|
||||
const region = fromOrder.region
|
||||
const client = await this.getClient()
|
||||
@@ -492,10 +1003,12 @@ class BrightpearlService extends BaseService {
|
||||
"billing_address",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"sales_channel",
|
||||
],
|
||||
})
|
||||
|
||||
const client = await this.getClient()
|
||||
|
||||
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
||||
|
||||
// All sales orders must have a customer
|
||||
@@ -506,13 +1019,17 @@ class BrightpearlService extends BaseService {
|
||||
const authData = await this.getAuthData()
|
||||
|
||||
const { shipping_address } = fromOrder
|
||||
|
||||
const order = {
|
||||
currency: {
|
||||
code: fromOrder.currency_code.toUpperCase(),
|
||||
},
|
||||
ref: fromOrder.display_id,
|
||||
externalRef: fromOrder.id,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
channelId:
|
||||
fromOrder.sales_channel?.metadata?.bp_id ||
|
||||
this.options.channel_id ||
|
||||
`1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
statusId: this.options.default_status_id || `3`,
|
||||
customer: {
|
||||
@@ -545,12 +1062,14 @@ class BrightpearlService extends BaseService {
|
||||
return client.orders
|
||||
.create(order)
|
||||
.then(async (salesOrderId) => {
|
||||
const order = await client.orders.retrieve(salesOrderId)
|
||||
await client.warehouses
|
||||
.createReservation(order, this.options.warehouse)
|
||||
.catch((err) => {
|
||||
console.log("Failed to allocate for order:", salesOrderId)
|
||||
})
|
||||
if (!this.inventoryService_) {
|
||||
const order = await client.orders.retrieve(salesOrderId)
|
||||
await client.warehouses
|
||||
.createReservation(order, this.options.warehouse)
|
||||
.catch((err) => {
|
||||
console.log("Failed to allocate for order:", salesOrderId)
|
||||
})
|
||||
}
|
||||
return salesOrderId
|
||||
})
|
||||
.then((salesOrderId) => {
|
||||
@@ -1157,7 +1676,7 @@ class BrightpearlService extends BaseService {
|
||||
}
|
||||
|
||||
async createFulfillmentFromGoodsOut(id) {
|
||||
await this.manager_.transaction(async (m) => {
|
||||
await this.manager_.transaction(async (transactionManager) => {
|
||||
const client = await this.getClient()
|
||||
|
||||
// Get goods out and associated order
|
||||
@@ -1165,7 +1684,19 @@ class BrightpearlService extends BaseService {
|
||||
const order = await client.orders.retrieve(goodsOut.orderId)
|
||||
|
||||
// Only relevant for medusa orders check channel id
|
||||
if (order.channelId !== parseInt(this.options.channel_id)) {
|
||||
const { fulfillments: existingFulfillments, sales_channel } =
|
||||
await this.orderService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(order.externalRef, {
|
||||
relations: ["fulfillments", "sales_channel"],
|
||||
})
|
||||
|
||||
if (
|
||||
(sales_channel.metadata?.bp_id &&
|
||||
sales_channel.metadata.bp_id !== order.channelId) ||
|
||||
(this.options.channel_id &&
|
||||
order.channelId !== parseInt(this.options.channel_id))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1194,28 +1725,95 @@ class BrightpearlService extends BaseService {
|
||||
|
||||
if (partId) {
|
||||
if (partId.startsWith("claim")) {
|
||||
return this.claimService_
|
||||
.withTransaction(m)
|
||||
return await this.claimService_
|
||||
.withTransaction(transactionManager)
|
||||
.createFulfillment(partId, {
|
||||
metadata: { goods_out_note: id },
|
||||
})
|
||||
} else {
|
||||
return this.swapService_
|
||||
.withTransaction(m)
|
||||
return await this.swapService_
|
||||
.withTransaction(transactionManager)
|
||||
.createFulfillment(partId, {
|
||||
metadata: { goods_out_note: id },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return this.orderService_
|
||||
.withTransaction(m)
|
||||
if (!(this.inventoryService_ && this.stockLocationService_)) {
|
||||
return await this.orderService_
|
||||
.withTransaction(transactionManager)
|
||||
.createFulfillment(order.externalRef, lines, {
|
||||
metadata: { goods_out_note: id },
|
||||
})
|
||||
}
|
||||
|
||||
const bpLocation = goodsOut.warehouseId
|
||||
|
||||
const fulfillmentLocation =
|
||||
await this.getMedusaLocationFromBrightPearlWarehouse(
|
||||
bpLocation,
|
||||
sales_channel.id,
|
||||
{ transactionManager: transactionManager }
|
||||
)
|
||||
|
||||
const medusaOrder = await this.orderService_
|
||||
.withTransaction(transactionManager)
|
||||
.createFulfillment(order.externalRef, lines, {
|
||||
metadata: { goods_out_note: id },
|
||||
location_id: fulfillmentLocation.id,
|
||||
})
|
||||
|
||||
const existingFulfillmentMap = new Map(
|
||||
existingFulfillments.map((fulfillment) => [fulfillment.id, fulfillment])
|
||||
)
|
||||
|
||||
const { fulfillments } = await this.orderService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(order.externalRef, {
|
||||
relations: [
|
||||
"fulfillments",
|
||||
"fulfillments.items",
|
||||
"fulfillments.items.item",
|
||||
],
|
||||
})
|
||||
|
||||
await updateInventoryAndReservations(
|
||||
fulfillments.filter((f) => !existingFulfillmentMap.get(f.id)),
|
||||
{
|
||||
inventoryService:
|
||||
this.productVariantInventoryService_.withTransaction(
|
||||
transactionManager
|
||||
),
|
||||
locationId: fulfillmentLocation.id,
|
||||
}
|
||||
)
|
||||
|
||||
return medusaOrder
|
||||
}, "SERIALIZABLE")
|
||||
}
|
||||
|
||||
async getMedusaLocationFromBrightPearlWarehouse(
|
||||
bpLocationId,
|
||||
sales_channel_id,
|
||||
context
|
||||
) {
|
||||
const locationIds = await this.salesChannelLocationService_
|
||||
.withTransaction(context.transactionManager)
|
||||
.listLocationIds(sales_channel_id)
|
||||
|
||||
const locations = await this.stockLocationService_.list(
|
||||
{ id: locationIds },
|
||||
{},
|
||||
{ transactionManager: context.transactionManager }
|
||||
)
|
||||
|
||||
const fulfillmentLocation = locations.find(
|
||||
(location) => location.metadata?.bp_id === bpLocationId
|
||||
)
|
||||
|
||||
return fulfillmentLocation
|
||||
}
|
||||
|
||||
async createCustomer(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
const address = await client.addresses.create({
|
||||
|
||||
@@ -45,6 +45,24 @@ class OrderSubscriber {
|
||||
this.registerSwapPayment
|
||||
)
|
||||
eventBusService.subscribe("swap.received", this.registerSwap)
|
||||
|
||||
eventBusService.subscribe(
|
||||
"reservation-item.created",
|
||||
this.registerMedusaReservation
|
||||
)
|
||||
|
||||
eventBusService.subscribe(
|
||||
"reservation-items.bulk-created",
|
||||
this.registerMedusaBulkReservation
|
||||
)
|
||||
}
|
||||
|
||||
registerMedusaReservation = (data) => {
|
||||
return this.brightpearlService_.createReservation(data)
|
||||
}
|
||||
|
||||
registerMedusaBulkReservation = (data) => {
|
||||
return this.brightpearlService_.bulkCreateReservation(data)
|
||||
}
|
||||
|
||||
sendToBrightpearl = (data) => {
|
||||
|
||||
@@ -140,6 +140,12 @@ class BrightpearlClient {
|
||||
data,
|
||||
})
|
||||
},
|
||||
delete: (id) => {
|
||||
return this.client_.request({
|
||||
url: `/integration-service/webhook/${id}`,
|
||||
method: "DELETE",
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +204,15 @@ class BrightpearlClient {
|
||||
data,
|
||||
})
|
||||
},
|
||||
updateReservation: (orderId, data) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/order/${orderId}/reservation`,
|
||||
method: "PUT",
|
||||
data,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
createReservation: (order, warehouse) => {
|
||||
const id = order.id
|
||||
const data = order.rows.map((r) => ({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||
|
||||
export const featureFlag = "sales_channels"
|
||||
export const featureFlag = SalesChannelFeatureFlag.key
|
||||
|
||||
export class salesChannel1656949291839 implements MigrationInterface {
|
||||
name = "salesChannel1656949291839"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||
|
||||
export const featureFlag = SalesChannelFeatureFlag.key
|
||||
|
||||
export class addSalesChannelMetadata1680714052628
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "addSalesChannelMetadata1680714052628"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sales_channel" ADD COLUMN "metadata" jsonb NULL;`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sales_channel" DROP COLUMN "metadata"`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { BeforeInsert, Column, OneToMany } from "typeorm"
|
||||
|
||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||
import { SoftDeletableEntity } from "../interfaces"
|
||||
import { generateEntityId } from "../utils"
|
||||
import { DbAwareColumn, generateEntityId } from "../utils"
|
||||
import { SalesChannelLocation } from "./sales-channel-location"
|
||||
|
||||
@FeatureFlagEntity("sales_channels")
|
||||
@@ -16,6 +16,9 @@ export class SalesChannel extends SoftDeletableEntity {
|
||||
@Column({ default: false })
|
||||
is_disabled: boolean
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown> | null
|
||||
|
||||
@OneToMany(
|
||||
() => SalesChannelLocation,
|
||||
(scLocation) => scLocation.sales_channel,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EntityManager, In } from "typeorm"
|
||||
import {
|
||||
ICacheService,
|
||||
IEventBusService,
|
||||
IInventoryService,
|
||||
IStockLocationService,
|
||||
InventoryItemDTO,
|
||||
@@ -23,14 +24,19 @@ type InjectedDependencies = {
|
||||
productVariantService: ProductVariantService
|
||||
stockLocationService: IStockLocationService
|
||||
inventoryService: IInventoryService
|
||||
eventBusService: IEventBusService
|
||||
}
|
||||
|
||||
class ProductVariantInventoryService extends TransactionBaseService {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly salesChannelLocationService_: SalesChannelLocationService
|
||||
protected readonly salesChannelInventoryService_: SalesChannelInventoryService
|
||||
protected readonly productVariantService_: ProductVariantService
|
||||
protected readonly stockLocationService_: IStockLocationService
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
protected readonly eventBusService_: IEventBusService
|
||||
protected readonly cacheService_: ICacheService
|
||||
|
||||
constructor({
|
||||
@@ -39,6 +45,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
salesChannelInventoryService,
|
||||
productVariantService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -48,6 +55,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
this.stockLocationService_ = stockLocationService
|
||||
this.productVariantService_ = productVariantService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.eventBusService_ = eventBusService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +116,10 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
locationIds = stockLocations.map((l) => l.id)
|
||||
}
|
||||
|
||||
if (locationIds.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasInventory = await Promise.all(
|
||||
variantInventory.map(async (inventoryPart) => {
|
||||
const itemQuantity = inventoryPart.required_quantity * quantity
|
||||
@@ -346,7 +358,6 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
const toReserve = {
|
||||
type: "order",
|
||||
line_item_id: context.lineItemId,
|
||||
}
|
||||
|
||||
@@ -385,7 +396,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
locationId = locations[0].location_id
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
const reservationItems = await Promise.all(
|
||||
variantInventory.map(async (inventoryPart) => {
|
||||
const itemQuantity = inventoryPart.required_quantity * quantity
|
||||
return await this.inventoryService_.createReservationItem({
|
||||
@@ -396,6 +407,8 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return reservationItems
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,11 @@ import {
|
||||
AbstractCartCompletionStrategy,
|
||||
CartCompletionResponse,
|
||||
} from "../interfaces"
|
||||
import { IInventoryService, ReservationItemDTO } from "@medusajs/types"
|
||||
import {
|
||||
IEventBusService,
|
||||
IInventoryService,
|
||||
ReservationItemDTO,
|
||||
} from "@medusajs/types"
|
||||
import { IdempotencyKey, Order } from "../models"
|
||||
import OrderService, {
|
||||
ORDER_CART_ALREADY_EXISTS_ERROR,
|
||||
@@ -28,6 +32,7 @@ type InjectedDependencies = {
|
||||
swapService: SwapService
|
||||
manager: EntityManager
|
||||
inventoryService: IInventoryService
|
||||
eventBusService: IEventBusService
|
||||
}
|
||||
|
||||
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
@@ -39,6 +44,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
protected readonly orderService_: OrderService
|
||||
protected readonly swapService_: SwapService
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
protected readonly eventBusService_: IEventBusService
|
||||
|
||||
constructor({
|
||||
productVariantInventoryService,
|
||||
@@ -48,6 +54,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
orderService,
|
||||
swapService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -59,6 +66,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
this.orderService_ = orderService
|
||||
this.swapService_ = swapService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.eventBusService_ = eventBusService
|
||||
}
|
||||
|
||||
async complete(
|
||||
@@ -384,6 +392,14 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
} else if (this.inventoryService_) {
|
||||
await this.eventBusService_.emit("reservation-items.bulk-created", {
|
||||
ids: reservations
|
||||
.filter(([reservation]) => !!reservation)
|
||||
.flatMap(([reservationItemArr]) =>
|
||||
reservationItemArr!.map((item) => item.id)
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ export type CreateReservationItemInput = {
|
||||
location_id: string
|
||||
quantity: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
external_id?: string
|
||||
}
|
||||
|
||||
export type FilterableInventoryLevelProps = {
|
||||
|
||||
Reference in New Issue
Block a user