feat(medusa, medusa-plugin-brightpearl): Inventory management for Brightpearl (#3192)
This commit is contained in:
1 parent
8b6464180a
commit
4a85627435
20 files changed
+798
-170
No files matched your search
@@ -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`] = `
|
exports[`sales channels POST /admin/sales-channels successfully creates a disabled sales channel 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"sales_channel": ObjectContaining {
|
"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.status).toEqual(200)
|
||||||
expect(response.data.sales_channel).toBeTruthy()
|
expect(response.data.sales_channel).toBeTruthy()
|
||||||
expect(response.data.sales_channel).toMatchSnapshot({
|
expect(response.data.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: salesChannel.name,
|
name: salesChannel.name,
|
||||||
description: salesChannel.description,
|
description: salesChannel.description,
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -135,12 +137,12 @@ describe("sales channels", () => {
|
|||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.sales_channels).toBeTruthy()
|
expect(response.data.sales_channels).toBeTruthy()
|
||||||
expect(response.data.sales_channels.length).toBe(1)
|
expect(response.data.sales_channels.length).toBe(1)
|
||||||
expect(response.data).toMatchSnapshot({
|
expect(response.data).toEqual({
|
||||||
count: 1,
|
count: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
sales_channels: expect.arrayContaining([
|
sales_channels: expect.arrayContaining([
|
||||||
{
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: salesChannel2.name,
|
name: salesChannel2.name,
|
||||||
description: salesChannel2.description,
|
description: salesChannel2.description,
|
||||||
@@ -148,7 +150,7 @@ describe("sales channels", () => {
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
},
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -163,12 +165,12 @@ describe("sales channels", () => {
|
|||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.sales_channels).toBeTruthy()
|
expect(response.data.sales_channels).toBeTruthy()
|
||||||
expect(response.data.sales_channels.length).toBe(1)
|
expect(response.data.sales_channels.length).toBe(1)
|
||||||
expect(response.data).toMatchSnapshot({
|
expect(response.data).toEqual({
|
||||||
count: 1,
|
count: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
sales_channels: expect.arrayContaining([
|
sales_channels: expect.arrayContaining([
|
||||||
{
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: salesChannel1.name,
|
name: salesChannel1.name,
|
||||||
description: salesChannel1.description,
|
description: salesChannel1.description,
|
||||||
@@ -176,7 +178,7 @@ describe("sales channels", () => {
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
},
|
}),
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -218,7 +220,8 @@ describe("sales channels", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.sales_channel).toMatchSnapshot({
|
expect(response.data.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
@@ -226,6 +229,7 @@ describe("sales channels", () => {
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -445,7 +449,8 @@ describe("sales channels", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response.data.order.sales_channel).toBeTruthy()
|
expect(response.data.order.sales_channel).toBeTruthy()
|
||||||
expect(response.data.order.sales_channel).toMatchSnapshot({
|
expect(response.data.order.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "test name",
|
name: "test name",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
@@ -453,6 +458,7 @@ describe("sales channels", () => {
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -482,7 +488,8 @@ describe("sales channels", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response.data.orders[0].sales_channel).toBeTruthy()
|
expect(response.data.orders[0].sales_channel).toBeTruthy()
|
||||||
expect(response.data.orders[0].sales_channel).toMatchSnapshot({
|
expect(response.data.orders[0].sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "test name",
|
name: "test name",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
@@ -490,6 +497,7 @@ describe("sales channels", () => {
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -712,7 +720,8 @@ describe("sales channels", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.sales_channel).toEqual({
|
expect(response.data.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "test name",
|
name: "test name",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
@@ -721,6 +730,7 @@ describe("sales channels", () => {
|
|||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const attachedProduct = await dbConnection.manager.findOne(Product, {
|
const attachedProduct = await dbConnection.manager.findOne(Product, {
|
||||||
where: { id: product.id },
|
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,7 +281,8 @@ describe("sales channels", () => {
|
|||||||
const response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
const response = await api.get(`/store/carts/${cart.id}`, adminReqConfig)
|
||||||
|
|
||||||
expect(response.data.cart.sales_channel).toBeTruthy()
|
expect(response.data.cart.sales_channel).toBeTruthy()
|
||||||
expect(response.data.cart.sales_channel).toMatchSnapshot({
|
expect(response.data.cart.sales_channel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "test name",
|
name: "test name",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
@@ -289,6 +290,7 @@ describe("sales channels", () => {
|
|||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
})
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import * as setup from "./schema-migrations/1665748086258-inventory_setup"
|
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 UNIQUE INDEX "IDX_inventory_item_sku" ON "inventory_item" ("sku") WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "reservation_item" (
|
CREATE TABLE "reservation_item" (
|
||||||
"id" character varying NOT NULL,
|
"id" character varying NOT NULL,
|
||||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
|||||||
+17
@@ -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-item"
|
||||||
export * from "./inventory-level"
|
export * from "./inventory-level"
|
||||||
@@ -18,6 +18,9 @@ export class ReservationItem extends SoftDeletableEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
quantity: number
|
quantity: number
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
external_id: string | null
|
||||||
|
|
||||||
@Column({ type: "jsonb", nullable: true })
|
@Column({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown> | null
|
metadata: Record<string, unknown> | null
|
||||||
|
|
||||||
|
|||||||
@@ -132,18 +132,19 @@ export default class ReservationItemService {
|
|||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
): Promise<ReservationItem> {
|
): Promise<ReservationItem> {
|
||||||
const manager = context.transactionManager!
|
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,
|
inventory_item_id: data.inventory_item_id,
|
||||||
line_item_id: data.line_item_id,
|
line_item_id: data.line_item_id,
|
||||||
location_id: data.location_id,
|
location_id: data.location_id,
|
||||||
quantity: data.quantity,
|
quantity: data.quantity,
|
||||||
metadata: data.metadata,
|
metadata: data.metadata,
|
||||||
|
external_id: data.external_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [newInventoryItem] = await Promise.all([
|
const [newReservationItem] = await Promise.all([
|
||||||
itemRepository.save(inventoryItem),
|
reservationItemRepository.save(reservationItem),
|
||||||
this.inventoryLevelService_.adjustReservedQuantity(
|
this.inventoryLevelService_.adjustReservedQuantity(
|
||||||
data.inventory_item_id,
|
data.inventory_item_id,
|
||||||
data.location_id,
|
data.location_id,
|
||||||
@@ -153,10 +154,10 @@ export default class ReservationItemService {
|
|||||||
])
|
])
|
||||||
|
|
||||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.CREATED, {
|
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 { MedusaError, humanizeAmount } from "medusa-core-utils"
|
||||||
|
import {
|
||||||
|
ReservationType,
|
||||||
|
updateInventoryAndReservations,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
import Brightpearl from "../utils/brightpearl"
|
import Brightpearl from "../utils/brightpearl"
|
||||||
|
|
||||||
@@ -14,6 +19,13 @@ class BrightpearlService extends BaseService {
|
|||||||
swapService,
|
swapService,
|
||||||
claimService,
|
claimService,
|
||||||
discountService,
|
discountService,
|
||||||
|
stockLocationService,
|
||||||
|
inventoryService,
|
||||||
|
lineItemService,
|
||||||
|
eventBusService,
|
||||||
|
productVariantInventoryService,
|
||||||
|
salesChannelLocationService,
|
||||||
|
logger,
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
@@ -22,6 +34,7 @@ class BrightpearlService extends BaseService {
|
|||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.options = options
|
this.options = options
|
||||||
this.productVariantService_ = productVariantService
|
this.productVariantService_ = productVariantService
|
||||||
|
this.productVariantInventoryService_ = productVariantInventoryService
|
||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
this.totalsService_ = totalsService
|
this.totalsService_ = totalsService
|
||||||
@@ -29,6 +42,12 @@ class BrightpearlService extends BaseService {
|
|||||||
this.oauthService_ = oauthService
|
this.oauthService_ = oauthService
|
||||||
this.swapService_ = swapService
|
this.swapService_ = swapService
|
||||||
this.claimService_ = claimService
|
this.claimService_ = claimService
|
||||||
|
this.stockLocationService_ = stockLocationService
|
||||||
|
this.inventoryService_ = inventoryService
|
||||||
|
this.lineItemService_ = lineItemService
|
||||||
|
this.eventBusService_ = eventBusService
|
||||||
|
this.salesChannelLocationService_ = salesChannelLocationService
|
||||||
|
this.logger_ = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClient() {
|
async getClient() {
|
||||||
@@ -85,13 +104,14 @@ class BrightpearlService extends BaseService {
|
|||||||
|
|
||||||
async verifyWebhooks() {
|
async verifyWebhooks() {
|
||||||
const brightpearl = await this.getClient()
|
const brightpearl = await this.getClient()
|
||||||
|
|
||||||
const hooks = [
|
const hooks = [
|
||||||
{
|
{
|
||||||
subscribeTo: "goods-out-note.created",
|
subscribeTo: "goods-out-note.created",
|
||||||
httpMethod: "POST",
|
httpMethod: "POST",
|
||||||
uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`,
|
uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`,
|
||||||
bodyTemplate:
|
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",
|
contentType: "application/json",
|
||||||
idSetAccepted: false,
|
idSetAccepted: false,
|
||||||
},
|
},
|
||||||
@@ -166,36 +186,217 @@ class BrightpearlService extends BaseService {
|
|||||||
availabilities = Object.assign(availabilities, chunkAvails)
|
availabilities = Object.assign(availabilities, chunkAvails)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(
|
if (!this.inventoryService_) {
|
||||||
|
return await this.atomicPhase_(async (manager) => {
|
||||||
|
const [variants] = await this.productVariantService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.listAndCount({
|
||||||
|
sku: bpProducts.map(({ SKU }) => SKU),
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantsMap = new Map(
|
||||||
|
variants.filter((variant) => !!variant.sku).map((v) => [v.sku, v])
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantUpdates = await Promise.all(
|
||||||
bpProducts.map(async (bpProduct) => {
|
bpProducts.map(async (bpProduct) => {
|
||||||
const { SKU: sku, productId } = bpProduct
|
const { SKU: sku, productId } = bpProduct
|
||||||
|
|
||||||
const variant = await this.productVariantService_
|
const variant = variantsMap.get(sku)
|
||||||
.retrieveBySKU(sku, {
|
|
||||||
select: ["id", "manage_inventory", "inventory_quantity"],
|
|
||||||
})
|
|
||||||
.catch((_) => undefined)
|
|
||||||
|
|
||||||
const prodAvail = availabilities[productId]
|
const productAvailability = availabilities[productId]
|
||||||
|
|
||||||
let onHand = 0
|
let onHand = 0
|
||||||
if (
|
if (
|
||||||
prodAvail &&
|
productAvailability &&
|
||||||
prodAvail.warehouses &&
|
productAvailability.warehouses &&
|
||||||
prodAvail.warehouses[`${this.options.warehouse}`]
|
productAvailability.warehouses[`${this.options.warehouse}`]
|
||||||
) {
|
) {
|
||||||
onHand = prodAvail.warehouses[`${this.options.warehouse}`].onHand
|
onHand =
|
||||||
|
productAvailability.warehouses[`${this.options.warehouse}`]
|
||||||
|
.onHand
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variant && variant.manage_inventory) {
|
if (variant && variant.manage_inventory) {
|
||||||
if (parseInt(variant.inventory_quantity) !== parseInt(onHand)) {
|
if (parseInt(variant.inventory_quantity) !== parseInt(onHand)) {
|
||||||
return this.productVariantService_.update(variant.id, {
|
return {
|
||||||
inventory_quantity: parseInt(onHand),
|
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)
|
.retrieveAvailability(productId)
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
|
|
||||||
if (availability) {
|
if (!availability) {
|
||||||
const brightpearlProduct = await client.products.retrieve(productId)
|
return
|
||||||
|
|
||||||
const prodAvail = availability[productId]
|
|
||||||
|
|
||||||
let onHand = 0
|
|
||||||
if (
|
|
||||||
prodAvail.warehouses &&
|
|
||||||
prodAvail.warehouses[`${this.options.warehouse}`]
|
|
||||||
) {
|
|
||||||
onHand = prodAvail.warehouses[`${this.options.warehouse}`].onHand
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sku = brightpearlProduct.identity.sku
|
const brightpearlProduct = await client.products.retrieve(productId)
|
||||||
if (!sku) return
|
|
||||||
|
|
||||||
|
const sku = brightpearlProduct.identity.sku
|
||||||
|
if (!sku) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const productAvailability = availability[productId]
|
||||||
|
|
||||||
|
if (!this.inventoryService_) {
|
||||||
const variant = await this.productVariantService_
|
const variant = await this.productVariantService_
|
||||||
.retrieveBySKU(sku)
|
.retrieveBySKU(sku)
|
||||||
.catch((_) => undefined)
|
.catch((_) => undefined)
|
||||||
if (variant && variant.manage_inventory) {
|
|
||||||
await this.manager_.transaction((m) => {
|
if (!variant?.manage_inventory) {
|
||||||
return this.productVariantService_
|
return
|
||||||
.withTransaction(m)
|
|
||||||
.update(variant.id, {
|
|
||||||
inventory_quantity: onHand,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
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) {
|
async createSalesCredit(fromOrder, fromReturn) {
|
||||||
const region = fromOrder.region
|
const region = fromOrder.region
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
@@ -492,10 +1003,12 @@ class BrightpearlService extends BaseService {
|
|||||||
"billing_address",
|
"billing_address",
|
||||||
"shipping_methods",
|
"shipping_methods",
|
||||||
"payments",
|
"payments",
|
||||||
|
"sales_channel",
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
|
|
||||||
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
||||||
|
|
||||||
// All sales orders must have a customer
|
// All sales orders must have a customer
|
||||||
@@ -506,13 +1019,17 @@ class BrightpearlService extends BaseService {
|
|||||||
const authData = await this.getAuthData()
|
const authData = await this.getAuthData()
|
||||||
|
|
||||||
const { shipping_address } = fromOrder
|
const { shipping_address } = fromOrder
|
||||||
|
|
||||||
const order = {
|
const order = {
|
||||||
currency: {
|
currency: {
|
||||||
code: fromOrder.currency_code.toUpperCase(),
|
code: fromOrder.currency_code.toUpperCase(),
|
||||||
},
|
},
|
||||||
ref: fromOrder.display_id,
|
ref: fromOrder.display_id,
|
||||||
externalRef: fromOrder.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,
|
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||||
statusId: this.options.default_status_id || `3`,
|
statusId: this.options.default_status_id || `3`,
|
||||||
customer: {
|
customer: {
|
||||||
@@ -545,12 +1062,14 @@ class BrightpearlService extends BaseService {
|
|||||||
return client.orders
|
return client.orders
|
||||||
.create(order)
|
.create(order)
|
||||||
.then(async (salesOrderId) => {
|
.then(async (salesOrderId) => {
|
||||||
|
if (!this.inventoryService_) {
|
||||||
const order = await client.orders.retrieve(salesOrderId)
|
const order = await client.orders.retrieve(salesOrderId)
|
||||||
await client.warehouses
|
await client.warehouses
|
||||||
.createReservation(order, this.options.warehouse)
|
.createReservation(order, this.options.warehouse)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log("Failed to allocate for order:", salesOrderId)
|
console.log("Failed to allocate for order:", salesOrderId)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return salesOrderId
|
return salesOrderId
|
||||||
})
|
})
|
||||||
.then((salesOrderId) => {
|
.then((salesOrderId) => {
|
||||||
@@ -1157,7 +1676,7 @@ class BrightpearlService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createFulfillmentFromGoodsOut(id) {
|
async createFulfillmentFromGoodsOut(id) {
|
||||||
await this.manager_.transaction(async (m) => {
|
await this.manager_.transaction(async (transactionManager) => {
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
|
|
||||||
// Get goods out and associated order
|
// Get goods out and associated order
|
||||||
@@ -1165,7 +1684,19 @@ class BrightpearlService extends BaseService {
|
|||||||
const order = await client.orders.retrieve(goodsOut.orderId)
|
const order = await client.orders.retrieve(goodsOut.orderId)
|
||||||
|
|
||||||
// Only relevant for medusa orders check channel id
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,28 +1725,95 @@ class BrightpearlService extends BaseService {
|
|||||||
|
|
||||||
if (partId) {
|
if (partId) {
|
||||||
if (partId.startsWith("claim")) {
|
if (partId.startsWith("claim")) {
|
||||||
return this.claimService_
|
return await this.claimService_
|
||||||
.withTransaction(m)
|
.withTransaction(transactionManager)
|
||||||
.createFulfillment(partId, {
|
.createFulfillment(partId, {
|
||||||
metadata: { goods_out_note: id },
|
metadata: { goods_out_note: id },
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return this.swapService_
|
return await this.swapService_
|
||||||
.withTransaction(m)
|
.withTransaction(transactionManager)
|
||||||
.createFulfillment(partId, {
|
.createFulfillment(partId, {
|
||||||
metadata: { goods_out_note: id },
|
metadata: { goods_out_note: id },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.orderService_
|
if (!(this.inventoryService_ && this.stockLocationService_)) {
|
||||||
.withTransaction(m)
|
return await this.orderService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
.createFulfillment(order.externalRef, lines, {
|
.createFulfillment(order.externalRef, lines, {
|
||||||
metadata: { goods_out_note: id },
|
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")
|
}, "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) {
|
async createCustomer(fromOrder) {
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
const address = await client.addresses.create({
|
const address = await client.addresses.create({
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ class OrderSubscriber {
|
|||||||
this.registerSwapPayment
|
this.registerSwapPayment
|
||||||
)
|
)
|
||||||
eventBusService.subscribe("swap.received", this.registerSwap)
|
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) => {
|
sendToBrightpearl = (data) => {
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ class BrightpearlClient {
|
|||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
delete: (id) => {
|
||||||
|
return this.client_.request({
|
||||||
|
url: `/integration-service/webhook/${id}`,
|
||||||
|
method: "DELETE",
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +204,15 @@ class BrightpearlClient {
|
|||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateReservation: (orderId, data) => {
|
||||||
|
return this.client_
|
||||||
|
.request({
|
||||||
|
url: `/warehouse-service/order/${orderId}/reservation`,
|
||||||
|
method: "PUT",
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
.then(({ data }) => data.response)
|
||||||
|
},
|
||||||
createReservation: (order, warehouse) => {
|
createReservation: (order, warehouse) => {
|
||||||
const id = order.id
|
const id = order.id
|
||||||
const data = order.rows.map((r) => ({
|
const data = order.rows.map((r) => ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
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 {
|
export class salesChannel1656949291839 implements MigrationInterface {
|
||||||
name = "salesChannel1656949291839"
|
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 { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||||
import { SoftDeletableEntity } from "../interfaces"
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
import { generateEntityId } from "../utils"
|
import { DbAwareColumn, generateEntityId } from "../utils"
|
||||||
import { SalesChannelLocation } from "./sales-channel-location"
|
import { SalesChannelLocation } from "./sales-channel-location"
|
||||||
|
|
||||||
@FeatureFlagEntity("sales_channels")
|
@FeatureFlagEntity("sales_channels")
|
||||||
@@ -16,6 +16,9 @@ export class SalesChannel extends SoftDeletableEntity {
|
|||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
is_disabled: boolean
|
is_disabled: boolean
|
||||||
|
|
||||||
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
|
metadata: Record<string, unknown> | null
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => SalesChannelLocation,
|
() => SalesChannelLocation,
|
||||||
(scLocation) => scLocation.sales_channel,
|
(scLocation) => scLocation.sales_channel,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { EntityManager, In } from "typeorm"
|
import { EntityManager, In } from "typeorm"
|
||||||
import {
|
import {
|
||||||
ICacheService,
|
ICacheService,
|
||||||
|
IEventBusService,
|
||||||
IInventoryService,
|
IInventoryService,
|
||||||
IStockLocationService,
|
IStockLocationService,
|
||||||
InventoryItemDTO,
|
InventoryItemDTO,
|
||||||
@@ -23,14 +24,19 @@ type InjectedDependencies = {
|
|||||||
productVariantService: ProductVariantService
|
productVariantService: ProductVariantService
|
||||||
stockLocationService: IStockLocationService
|
stockLocationService: IStockLocationService
|
||||||
inventoryService: IInventoryService
|
inventoryService: IInventoryService
|
||||||
|
eventBusService: IEventBusService
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProductVariantInventoryService extends TransactionBaseService {
|
class ProductVariantInventoryService extends TransactionBaseService {
|
||||||
|
protected manager_: EntityManager
|
||||||
|
protected transactionManager_: EntityManager | undefined
|
||||||
|
|
||||||
protected readonly salesChannelLocationService_: SalesChannelLocationService
|
protected readonly salesChannelLocationService_: SalesChannelLocationService
|
||||||
protected readonly salesChannelInventoryService_: SalesChannelInventoryService
|
protected readonly salesChannelInventoryService_: SalesChannelInventoryService
|
||||||
protected readonly productVariantService_: ProductVariantService
|
protected readonly productVariantService_: ProductVariantService
|
||||||
protected readonly stockLocationService_: IStockLocationService
|
protected readonly stockLocationService_: IStockLocationService
|
||||||
protected readonly inventoryService_: IInventoryService
|
protected readonly inventoryService_: IInventoryService
|
||||||
|
protected readonly eventBusService_: IEventBusService
|
||||||
protected readonly cacheService_: ICacheService
|
protected readonly cacheService_: ICacheService
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@@ -39,6 +45,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
salesChannelInventoryService,
|
salesChannelInventoryService,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
inventoryService,
|
inventoryService,
|
||||||
|
eventBusService,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -48,6 +55,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
this.stockLocationService_ = stockLocationService
|
this.stockLocationService_ = stockLocationService
|
||||||
this.productVariantService_ = productVariantService
|
this.productVariantService_ = productVariantService
|
||||||
this.inventoryService_ = inventoryService
|
this.inventoryService_ = inventoryService
|
||||||
|
this.eventBusService_ = eventBusService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,6 +116,10 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
locationIds = stockLocations.map((l) => l.id)
|
locationIds = stockLocations.map((l) => l.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (locationIds.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const hasInventory = await Promise.all(
|
const hasInventory = await Promise.all(
|
||||||
variantInventory.map(async (inventoryPart) => {
|
variantInventory.map(async (inventoryPart) => {
|
||||||
const itemQuantity = inventoryPart.required_quantity * quantity
|
const itemQuantity = inventoryPart.required_quantity * quantity
|
||||||
@@ -346,7 +358,6 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toReserve = {
|
const toReserve = {
|
||||||
type: "order",
|
|
||||||
line_item_id: context.lineItemId,
|
line_item_id: context.lineItemId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +396,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
locationId = locations[0].location_id
|
locationId = locations[0].location_id
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Promise.all(
|
const reservationItems = await Promise.all(
|
||||||
variantInventory.map(async (inventoryPart) => {
|
variantInventory.map(async (inventoryPart) => {
|
||||||
const itemQuantity = inventoryPart.required_quantity * quantity
|
const itemQuantity = inventoryPart.required_quantity * quantity
|
||||||
return await this.inventoryService_.createReservationItem({
|
return await this.inventoryService_.createReservationItem({
|
||||||
@@ -396,6 +407,8 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return reservationItems
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import {
|
|||||||
AbstractCartCompletionStrategy,
|
AbstractCartCompletionStrategy,
|
||||||
CartCompletionResponse,
|
CartCompletionResponse,
|
||||||
} from "../interfaces"
|
} from "../interfaces"
|
||||||
import { IInventoryService, ReservationItemDTO } from "@medusajs/types"
|
import {
|
||||||
|
IEventBusService,
|
||||||
|
IInventoryService,
|
||||||
|
ReservationItemDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
import { IdempotencyKey, Order } from "../models"
|
import { IdempotencyKey, Order } from "../models"
|
||||||
import OrderService, {
|
import OrderService, {
|
||||||
ORDER_CART_ALREADY_EXISTS_ERROR,
|
ORDER_CART_ALREADY_EXISTS_ERROR,
|
||||||
@@ -28,6 +32,7 @@ type InjectedDependencies = {
|
|||||||
swapService: SwapService
|
swapService: SwapService
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
inventoryService: IInventoryService
|
inventoryService: IInventoryService
|
||||||
|
eventBusService: IEventBusService
|
||||||
}
|
}
|
||||||
|
|
||||||
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||||
@@ -39,6 +44,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
protected readonly orderService_: OrderService
|
protected readonly orderService_: OrderService
|
||||||
protected readonly swapService_: SwapService
|
protected readonly swapService_: SwapService
|
||||||
protected readonly inventoryService_: IInventoryService
|
protected readonly inventoryService_: IInventoryService
|
||||||
|
protected readonly eventBusService_: IEventBusService
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
productVariantInventoryService,
|
productVariantInventoryService,
|
||||||
@@ -48,6 +54,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
orderService,
|
orderService,
|
||||||
swapService,
|
swapService,
|
||||||
inventoryService,
|
inventoryService,
|
||||||
|
eventBusService,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -59,6 +66,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
this.swapService_ = swapService
|
this.swapService_ = swapService
|
||||||
this.inventoryService_ = inventoryService
|
this.inventoryService_ = inventoryService
|
||||||
|
this.eventBusService_ = eventBusService
|
||||||
}
|
}
|
||||||
|
|
||||||
async complete(
|
async complete(
|
||||||
@@ -384,6 +392,14 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
} else {
|
} else {
|
||||||
throw error
|
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
|
location_id: string
|
||||||
quantity: number
|
quantity: number
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
|
external_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterableInventoryLevelProps = {
|
export type FilterableInventoryLevelProps = {
|
||||||
|
|||||||
Reference in new issue
Block a user