feat(medusa,stock-location,inventory): Integration tests (#3149)
This commit is contained in:
1 parent
71fdd28198
commit
923ccece24
17 files changed
+2018
-31
No files matched your search
@@ -0,0 +1,270 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../../helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
|
const cartSeeder = require("../../../helpers/cart-seeder")
|
||||||
|
const { simpleProductFactory } = require("../../../../api/factories")
|
||||||
|
const { simpleSalesChannelFactory } = require("../../../../api/factories")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
const adminHeaders = { headers: { Authorization: "Bearer test_token" } }
|
||||||
|
|
||||||
|
describe("/store/carts", () => {
|
||||||
|
let express
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
|
||||||
|
let variantId
|
||||||
|
let inventoryItemId
|
||||||
|
let locationId
|
||||||
|
|
||||||
|
const doAfterEach = async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /store/carts/:id", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
id: "test-channel",
|
||||||
|
is_default: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
await cartSeeder(dbConnection, { sales_channel_id: "test-channel" })
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "product1",
|
||||||
|
sales_channels: [{ id: "test-channel" }],
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
// Add payment provider
|
||||||
|
await api.post(
|
||||||
|
`/admin/regions/test-region/payment-providers`,
|
||||||
|
{
|
||||||
|
provider_id: "test-pay",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const prodVarInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/product1/variants`,
|
||||||
|
{
|
||||||
|
title: "Test Variant w. inventory",
|
||||||
|
sku: "MY_SKU",
|
||||||
|
material: "material",
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "mids",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: "product1-option",
|
||||||
|
value: "SS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
manage_inventory: true,
|
||||||
|
prices: [{ currency_code: "usd", amount: 2300 }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const variant = response.data.product.variants[0]
|
||||||
|
|
||||||
|
variantId = variant.id
|
||||||
|
|
||||||
|
const inventoryItems =
|
||||||
|
await prodVarInventoryService.listInventoryItemsByVariant(variantId)
|
||||||
|
|
||||||
|
inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
|
// Add Stock location
|
||||||
|
const stockRes = await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{
|
||||||
|
name: "Fake Warehouse",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
locationId = stockRes.data.stock_location.id
|
||||||
|
|
||||||
|
// Add stock level
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
// Associate Stock Location with sales channel
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/test-channel/stock-locations`,
|
||||||
|
{
|
||||||
|
location_id: locationId,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await doAfterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("reserve quantity when completing the cart", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const cartId = "test-cart"
|
||||||
|
|
||||||
|
// Add standard line item to cart
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cartId}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: variantId,
|
||||||
|
quantity: 3,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cartId}/payment-sessions`)
|
||||||
|
await api.post(`/store/carts/${cartId}/payment-session`, {
|
||||||
|
provider_id: "test-pay",
|
||||||
|
})
|
||||||
|
|
||||||
|
const getRes = await api.post(`/store/carts/${cartId}/complete`)
|
||||||
|
|
||||||
|
expect(getRes.status).toEqual(200)
|
||||||
|
expect(getRes.data.type).toEqual("order")
|
||||||
|
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
const stockLevel = await inventoryService.retrieveInventoryLevel(
|
||||||
|
inventoryItemId,
|
||||||
|
locationId
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stockLevel.location_id).toEqual(locationId)
|
||||||
|
expect(stockLevel.inventory_item_id).toEqual(inventoryItemId)
|
||||||
|
expect(stockLevel.reserved_quantity).toEqual(3)
|
||||||
|
expect(stockLevel.stocked_quantity).toEqual(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to add a item on the cart if the inventory isn't enough", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const cartId = "test-cart"
|
||||||
|
|
||||||
|
// Add standard line item to cart
|
||||||
|
const addCart = await api
|
||||||
|
.post(
|
||||||
|
`/store/carts/${cartId}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: variantId,
|
||||||
|
quantity: 6,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(addCart.response.status).toEqual(400)
|
||||||
|
expect(addCart.response.data.code).toEqual("insufficient_inventory")
|
||||||
|
expect(addCart.response.data.message).toEqual(
|
||||||
|
`Variant with id: ${variantId} does not have the required inventory`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to complete cart with items inventory not covered", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const cartId = "test-cart"
|
||||||
|
|
||||||
|
// Add standard line item to cart
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cartId}/line-items`,
|
||||||
|
{
|
||||||
|
variant_id: variantId,
|
||||||
|
quantity: 5,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cartId}/payment-sessions`)
|
||||||
|
await api.post(`/store/carts/${cartId}/payment-session`, {
|
||||||
|
provider_id: "test-pay",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Another proccess reserves items before the cart is completed
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
inventoryService.createReservationItem({
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItemId,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
const completeCartRes = await api
|
||||||
|
.post(`/store/carts/${cartId}/complete`)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(completeCartRes.response.status).toEqual(409)
|
||||||
|
expect(completeCartRes.response.data.code).toEqual(
|
||||||
|
"insufficient_inventory"
|
||||||
|
)
|
||||||
|
expect(completeCartRes.response.data.message).toEqual(
|
||||||
|
`Variant with id: ${variantId} does not have the required inventory`
|
||||||
|
)
|
||||||
|
|
||||||
|
const stockLevel = await inventoryService.retrieveInventoryLevel(
|
||||||
|
inventoryItemId,
|
||||||
|
locationId
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stockLevel.location_id).toEqual(locationId)
|
||||||
|
expect(stockLevel.inventory_item_id).toEqual(inventoryItemId)
|
||||||
|
expect(stockLevel.reserved_quantity).toEqual(2)
|
||||||
|
expect(stockLevel.stocked_quantity).toEqual(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../../helpers/use-api")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
const { simpleProductFactory } = require("../../../factories")
|
||||||
|
const adminHeaders = { headers: { Authorization: "Bearer test_token" } }
|
||||||
|
|
||||||
|
describe("Inventory Items endpoints", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
let variantId
|
||||||
|
let inventoryItems
|
||||||
|
let locationId
|
||||||
|
let location2Id
|
||||||
|
let location3Id
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// create inventory item
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-product",
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
const prodVarInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/test-product/variants`,
|
||||||
|
{
|
||||||
|
title: "Test Variant w. inventory",
|
||||||
|
sku: "MY_SKU",
|
||||||
|
material: "material",
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "mids",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
manage_inventory: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: "test-product-option",
|
||||||
|
value: "SS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [{ currency_code: "usd", amount: 2300 }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const variant = response.data.product.variants[0]
|
||||||
|
|
||||||
|
variantId = variant.id
|
||||||
|
|
||||||
|
inventoryItems = await prodVarInventoryService.listInventoryItemsByVariant(
|
||||||
|
variantId
|
||||||
|
)
|
||||||
|
|
||||||
|
const stockRes = await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{
|
||||||
|
name: "Fake Warehouse",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
locationId = stockRes.data.stock_location.id
|
||||||
|
|
||||||
|
const secondStockRes = await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{
|
||||||
|
name: "Another random Warehouse",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
location2Id = secondStockRes.data.stock_location.id
|
||||||
|
|
||||||
|
const thirdStockRes = await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{
|
||||||
|
name: "Another random Warehouse",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
location3Id = thirdStockRes.data.stock_location.id
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Inventory Items", () => {
|
||||||
|
it("Create, update and delete inventory location level", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 17,
|
||||||
|
incoming_quantity: 2,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
const stockLevel = await inventoryService.retrieveInventoryLevel(
|
||||||
|
inventoryItemId,
|
||||||
|
locationId
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(stockLevel.location_id).toEqual(locationId)
|
||||||
|
expect(stockLevel.inventory_item_id).toEqual(inventoryItemId)
|
||||||
|
expect(stockLevel.stocked_quantity).toEqual(17)
|
||||||
|
expect(stockLevel.incoming_quantity).toEqual(2)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
||||||
|
{
|
||||||
|
stocked_quantity: 21,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const newStockLevel = await inventoryService.retrieveInventoryLevel(
|
||||||
|
inventoryItemId,
|
||||||
|
locationId
|
||||||
|
)
|
||||||
|
expect(newStockLevel.stocked_quantity).toEqual(21)
|
||||||
|
expect(newStockLevel.incoming_quantity).toEqual(0)
|
||||||
|
|
||||||
|
await api.delete(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels/${locationId}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
const invLevel = await inventoryService
|
||||||
|
.retrieveInventoryLevel(inventoryItemId, locationId)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(invLevel.message).toEqual(
|
||||||
|
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Update inventory item", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}`,
|
||||||
|
{
|
||||||
|
mid_code: "updated mid_code",
|
||||||
|
weight: 120,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.inventory_item).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "updated mid_code",
|
||||||
|
weight: 120,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Retrieve an inventory item", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 15,
|
||||||
|
incoming_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 7,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
inventory_item: expect.objectContaining({
|
||||||
|
height: 200,
|
||||||
|
hs_code: "hs001",
|
||||||
|
id: inventoryItemId,
|
||||||
|
length: 100,
|
||||||
|
location_levels: [
|
||||||
|
expect.objectContaining({
|
||||||
|
available_quantity: 15,
|
||||||
|
deleted_at: null,
|
||||||
|
id: expect.any(String),
|
||||||
|
incoming_quantity: 5,
|
||||||
|
inventory_item_id: inventoryItemId,
|
||||||
|
location_id: locationId,
|
||||||
|
metadata: null,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
stocked_quantity: 15,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
available_quantity: 7,
|
||||||
|
deleted_at: null,
|
||||||
|
id: expect.any(String),
|
||||||
|
incoming_quantity: 0,
|
||||||
|
inventory_item_id: inventoryItemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
metadata: null,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
stocked_quantity: 7,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
material: "material",
|
||||||
|
metadata: null,
|
||||||
|
mid_code: "mids",
|
||||||
|
origin_country: "UK",
|
||||||
|
requires_shipping: true,
|
||||||
|
sku: "MY_SKU",
|
||||||
|
weight: 300,
|
||||||
|
width: 150,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("List inventory items", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItemId}/location-levels`,
|
||||||
|
{
|
||||||
|
location_id: location3Id,
|
||||||
|
stocked_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/admin/inventory-items`, adminHeaders)
|
||||||
|
|
||||||
|
expect(response.data.inventory_items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: inventoryItemId,
|
||||||
|
sku: "MY_SKU",
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "mids",
|
||||||
|
material: "material",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
requires_shipping: true,
|
||||||
|
metadata: null,
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: variantId,
|
||||||
|
title: "Test Variant w. inventory",
|
||||||
|
product_id: "test-product",
|
||||||
|
sku: "MY_SKU",
|
||||||
|
manage_inventory: true,
|
||||||
|
hs_code: "hs001",
|
||||||
|
origin_country: "UK",
|
||||||
|
mid_code: "mids",
|
||||||
|
material: "material",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
metadata: null,
|
||||||
|
product: expect.objectContaining({
|
||||||
|
id: "test-product",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
location_levels: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
inventory_item_id: inventoryItemId,
|
||||||
|
location_id: location2Id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
metadata: null,
|
||||||
|
available_quantity: 10,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
inventory_item_id: inventoryItemId,
|
||||||
|
location_id: location3Id,
|
||||||
|
stocked_quantity: 5,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
metadata: null,
|
||||||
|
available_quantity: 5,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
const { setPort, useApi } = require("../../../../helpers/use-api")
|
||||||
|
|
||||||
|
const {
|
||||||
|
ProductVariantInventoryService,
|
||||||
|
ProductVariantService,
|
||||||
|
} = require("@medusajs/medusa")
|
||||||
|
|
||||||
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
const { simpleProductFactory } = require("../../../factories")
|
||||||
|
|
||||||
|
describe("Create Variant", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
setPort(port)
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Inventory Items", () => {
|
||||||
|
it("When creating a new product variant it should create an Inventory Item", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-product",
|
||||||
|
variants: [{ id: "test-variant" }],
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/test-product/variants`,
|
||||||
|
{
|
||||||
|
title: "Test Variant w. inventory",
|
||||||
|
sku: "MY_SKU",
|
||||||
|
material: "material",
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "mids",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
manage_inventory: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: "test-product-option",
|
||||||
|
value: "SS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [{ currency_code: "usd", amount: 2300 }],
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: "Bearer test_token" } }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
const variantId = response.data.product.variants.find(
|
||||||
|
(v) => v.id !== "test-variant"
|
||||||
|
).id
|
||||||
|
|
||||||
|
const variantInventoryService = appContainer.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
const inventory =
|
||||||
|
await variantInventoryService.listInventoryItemsByVariant(variantId)
|
||||||
|
|
||||||
|
expect(inventory).toHaveLength(1)
|
||||||
|
expect(inventory).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "mids",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("When creating a new variant fails, it should revert all the transaction", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await simpleProductFactory(
|
||||||
|
dbConnection,
|
||||||
|
{
|
||||||
|
id: "test-product",
|
||||||
|
variants: [{ id: "test-variant" }],
|
||||||
|
},
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(ProductVariantInventoryService.prototype, "attachInventoryItem")
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new Error("Failure while attaching inventory item")
|
||||||
|
})
|
||||||
|
|
||||||
|
const prodVariantDeleteMock = jest.spyOn(
|
||||||
|
ProductVariantService.prototype,
|
||||||
|
"delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
const error = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/test-product/variants`,
|
||||||
|
{
|
||||||
|
title: "Test Variant w. inventory",
|
||||||
|
sku: "MY_SKU",
|
||||||
|
material: "material",
|
||||||
|
origin_country: "UK",
|
||||||
|
hs_code: "hs001",
|
||||||
|
mid_code: "mids",
|
||||||
|
weight: 300,
|
||||||
|
length: 100,
|
||||||
|
height: 200,
|
||||||
|
width: 150,
|
||||||
|
manage_inventory: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
option_id: "test-product-option",
|
||||||
|
value: "SS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prices: [{ currency_code: "usd", amount: 2300 }],
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: "Bearer test_token" } }
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data.message).toEqual(
|
||||||
|
"Failure while attaching inventory item"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prodVariantDeleteMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,717 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
describe("Inventory Module", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Inventory Module Interface", () => {
|
||||||
|
it("createInventoryItem", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(inventoryItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
requires_shipping: true,
|
||||||
|
metadata: { abc: 123 },
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updateInventoryItem", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const item = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedInventoryItem = await inventoryService.updateInventoryItem(
|
||||||
|
item.id,
|
||||||
|
{
|
||||||
|
origin_country: "CZ",
|
||||||
|
mid_code: "mid code 345",
|
||||||
|
material: "lycra and polyester",
|
||||||
|
weight: 500,
|
||||||
|
metadata: {
|
||||||
|
dce: 456,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updatedInventoryItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: item.id,
|
||||||
|
sku: item.sku,
|
||||||
|
origin_country: "CZ",
|
||||||
|
hs_code: item.hs_code,
|
||||||
|
mid_code: "mid code 345",
|
||||||
|
material: "lycra and polyester",
|
||||||
|
weight: 500,
|
||||||
|
length: item.length,
|
||||||
|
height: item.height,
|
||||||
|
width: item.width,
|
||||||
|
requires_shipping: true,
|
||||||
|
metadata: { dce: 456 },
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deleteInventoryItem and retrieveInventoryItem", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const item = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.deleteInventoryItem(item.id)
|
||||||
|
|
||||||
|
const deletedItem = inventoryService.retrieveInventoryItem(item.id)
|
||||||
|
|
||||||
|
await expect(deletedItem).rejects.toThrow(
|
||||||
|
`InventoryItem with id ${item.id} was not found`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("createInventoryLevel", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const inventoryLevel = await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 15,
|
||||||
|
incoming_quantity: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "second_location",
|
||||||
|
stocked_quantity: 10,
|
||||||
|
reserved_quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(inventoryLevel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
incoming_quantity: 4,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
metadata: null,
|
||||||
|
reserved_quantity: 15,
|
||||||
|
stocked_quantity: 50,
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.retrieveStockedQuantity(inventoryItem.id, [
|
||||||
|
"location_123",
|
||||||
|
])
|
||||||
|
).toEqual(50)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.retrieveAvailableQuantity(inventoryItem.id, [
|
||||||
|
"location_123",
|
||||||
|
])
|
||||||
|
).toEqual(35)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.retrieveReservedQuantity(inventoryItem.id, [
|
||||||
|
"location_123",
|
||||||
|
])
|
||||||
|
).toEqual(15)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.retrieveStockedQuantity(inventoryItem.id, [
|
||||||
|
"location_123",
|
||||||
|
"second_location",
|
||||||
|
])
|
||||||
|
).toEqual(60)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.retrieveAvailableQuantity(inventoryItem.id, [
|
||||||
|
"location_123",
|
||||||
|
"second_location",
|
||||||
|
])
|
||||||
|
).toEqual(44)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.retrieveReservedQuantity(inventoryItem.id, [
|
||||||
|
"location_123",
|
||||||
|
"second_location",
|
||||||
|
])
|
||||||
|
).toEqual(16)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updateInventoryLevel", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedLevel = await inventoryService.updateInventoryLevel(
|
||||||
|
inventoryItem.id,
|
||||||
|
"location_123",
|
||||||
|
{
|
||||||
|
stocked_quantity: 25,
|
||||||
|
reserved_quantity: 4,
|
||||||
|
incoming_quantity: 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updatedLevel).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
incoming_quantity: 10,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
metadata: null,
|
||||||
|
reserved_quantity: 4,
|
||||||
|
stocked_quantity: 25,
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deleteInventoryLevel", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
origin_country: "CH",
|
||||||
|
mid_code: "mid code",
|
||||||
|
material: "lycra",
|
||||||
|
weight: 100,
|
||||||
|
length: 200,
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
hs_code: "hs_code 123",
|
||||||
|
requires_shipping: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const inventoryLevel = await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: "location_123",
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.deleteInventoryLevel(
|
||||||
|
inventoryItem.id,
|
||||||
|
"location_123"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deletedLevel = inventoryService.retrieveInventoryLevel(
|
||||||
|
inventoryItem.id,
|
||||||
|
"location_123"
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(deletedLevel).rejects.toThrow(
|
||||||
|
`Inventory level for item ${inventoryItem.id} and location location_123 not found`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("createReservationItem", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const locationId = "location_123"
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
})
|
||||||
|
|
||||||
|
const tryReserve = inventoryService.createReservationItem({
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 10,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(tryReserve).rejects.toThrow(
|
||||||
|
`Item ${inventoryItem.id} is not stocked at location ${locationId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const inventoryReservation = await inventoryService.createReservationItem(
|
||||||
|
{
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 10,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(inventoryReservation).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 10,
|
||||||
|
metadata: { abc: 123 },
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const [available, reserved] = await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(inventoryItem.id, [
|
||||||
|
locationId,
|
||||||
|
]),
|
||||||
|
inventoryService.retrieveReservedQuantity(inventoryItem.id, locationId),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(available).toEqual(40)
|
||||||
|
expect(reserved).toEqual(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updateReservationItem", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const locationId = "location_123"
|
||||||
|
const newLocationId = "location_new"
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 50,
|
||||||
|
reserved_quantity: 0,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: newLocationId,
|
||||||
|
stocked_quantity: 20,
|
||||||
|
reserved_quantity: 5,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const inventoryReservation = await inventoryService.createReservationItem(
|
||||||
|
{
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 15,
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const [available, reserved] = await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(inventoryItem.id, locationId),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(available).toEqual(35)
|
||||||
|
expect(reserved).toEqual(15)
|
||||||
|
|
||||||
|
const updatedReservation = await inventoryService.updateReservationItem(
|
||||||
|
inventoryReservation.id,
|
||||||
|
{
|
||||||
|
quantity: 5,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updatedReservation).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 5,
|
||||||
|
metadata: { abc: 123 },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const [newAvailable, newReserved] = await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(inventoryItem.id, locationId),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(newAvailable).toEqual(45)
|
||||||
|
expect(newReserved).toEqual(5)
|
||||||
|
|
||||||
|
const updatedReservationLocation =
|
||||||
|
await inventoryService.updateReservationItem(inventoryReservation.id, {
|
||||||
|
quantity: 12,
|
||||||
|
location_id: newLocationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updatedReservationLocation).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: newLocationId,
|
||||||
|
quantity: 12,
|
||||||
|
metadata: { abc: 123 },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const [
|
||||||
|
oldLocationAvailable,
|
||||||
|
oldLocationReserved,
|
||||||
|
newLocationAvailable,
|
||||||
|
newLocationReserved,
|
||||||
|
] = await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(inventoryItem.id, locationId),
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
newLocationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
newLocationId
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(oldLocationAvailable).toEqual(50)
|
||||||
|
expect(oldLocationReserved).toEqual(0)
|
||||||
|
expect(newLocationAvailable).toEqual(3)
|
||||||
|
expect(newLocationReserved).toEqual(17)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deleteReservationItem and deleteReservationItemsByLineItem", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const locationId = "location_123"
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const inventoryReservation = await inventoryService.createReservationItem(
|
||||||
|
{
|
||||||
|
line_item_id: "line_item_123",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let quant = 1; quant <= 3; quant++) {
|
||||||
|
await inventoryService.createReservationItem({
|
||||||
|
line_item_id: "line_item_444",
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [available, reserved] = await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(inventoryItem.id, locationId),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(available).toEqual(6)
|
||||||
|
expect(reserved).toEqual(4)
|
||||||
|
|
||||||
|
await inventoryService.deleteReservationItemsByLineItem("line_item_444")
|
||||||
|
const [afterDeleteLineitemAvailable, afterDeleteLineitemReserved] =
|
||||||
|
await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(afterDeleteLineitemAvailable).toEqual(9)
|
||||||
|
expect(afterDeleteLineitemReserved).toEqual(1)
|
||||||
|
|
||||||
|
await inventoryService.deleteReservationItem(inventoryReservation.id)
|
||||||
|
const [afterDeleteReservationAvailable, afterDeleteReservationReserved] =
|
||||||
|
await Promise.all([
|
||||||
|
inventoryService.retrieveAvailableQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
inventoryService.retrieveReservedQuantity(
|
||||||
|
inventoryItem.id,
|
||||||
|
locationId
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(afterDeleteReservationAvailable).toEqual(10)
|
||||||
|
expect(afterDeleteReservationReserved).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("confirmInventory", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const locationId = "location_123"
|
||||||
|
const secondLocationId = "location_551"
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
reserved_quantity: 5,
|
||||||
|
})
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: secondLocationId,
|
||||||
|
stocked_quantity: 6,
|
||||||
|
reserved_quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.confirmInventory(inventoryItem.id, locationId, 5)
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.confirmInventory(
|
||||||
|
inventoryItem.id,
|
||||||
|
[locationId, secondLocationId],
|
||||||
|
10
|
||||||
|
)
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.confirmInventory(inventoryItem.id, locationId, 6)
|
||||||
|
).toBeFalsy()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.confirmInventory(
|
||||||
|
inventoryItem.id,
|
||||||
|
[locationId, secondLocationId],
|
||||||
|
11
|
||||||
|
)
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("adjustInventory", async () => {
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const locationId = "location_123"
|
||||||
|
const secondLocationId = "location_551"
|
||||||
|
|
||||||
|
const inventoryItem = await inventoryService.createInventoryItem({
|
||||||
|
sku: "sku_1",
|
||||||
|
})
|
||||||
|
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
reserved_quantity: 5,
|
||||||
|
})
|
||||||
|
await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: secondLocationId,
|
||||||
|
stocked_quantity: 6,
|
||||||
|
reserved_quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.adjustInventory(inventoryItem.id, locationId, -5)
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 5,
|
||||||
|
reserved_quantity: 5,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
metadata: null,
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await inventoryService.adjustInventory(
|
||||||
|
inventoryItem.id,
|
||||||
|
secondLocationId,
|
||||||
|
-10
|
||||||
|
)
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
location_id: secondLocationId,
|
||||||
|
stocked_quantity: -4,
|
||||||
|
reserved_quantity: 1,
|
||||||
|
incoming_quantity: 0,
|
||||||
|
metadata: null,
|
||||||
|
deleted_at: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+8
-8
@@ -368,7 +368,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "test-variant",
|
"id": "test-variant",
|
||||||
"inventory_quantity": 11,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -486,7 +486,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "test-variant",
|
"id": "test-variant",
|
||||||
"inventory_quantity": 11,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -670,7 +670,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "test-variant",
|
"id": "test-variant",
|
||||||
"inventory_quantity": 11,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -836,7 +836,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "test-variant",
|
"id": "test-variant",
|
||||||
"inventory_quantity": 12,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -2022,7 +2022,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "variant-2",
|
"id": "variant-2",
|
||||||
"inventory_quantity": 9,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -2130,7 +2130,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "test-variant",
|
"id": "test-variant",
|
||||||
"inventory_quantity": 9,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -2252,7 +2252,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "variant-2",
|
"id": "variant-2",
|
||||||
"inventory_quantity": 9,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
@@ -2368,7 +2368,7 @@ Object {
|
|||||||
"height": null,
|
"height": null,
|
||||||
"hs_code": null,
|
"hs_code": null,
|
||||||
"id": "variant-2",
|
"id": "variant-2",
|
||||||
"inventory_quantity": 9,
|
"inventory_quantity": 10,
|
||||||
"length": null,
|
"length": null,
|
||||||
"manage_inventory": true,
|
"manage_inventory": true,
|
||||||
"material": null,
|
"material": null,
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
const { bootstrapApp } = require("../../../helpers/bootstrap-app")
|
||||||
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
describe("Inventory Module", () => {
|
||||||
|
let appContainer
|
||||||
|
let dbConnection
|
||||||
|
let express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd })
|
||||||
|
const { container, app, port } = await bootstrapApp({ cwd })
|
||||||
|
appContainer = container
|
||||||
|
|
||||||
|
express = app.listen(port, (err) => {
|
||||||
|
process.send(port)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
express.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Stock Location Module Interface", () => {
|
||||||
|
it("create", async () => {
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.create({
|
||||||
|
name: "first location",
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: "first location",
|
||||||
|
deleted_at: null,
|
||||||
|
address_id: null,
|
||||||
|
metadata: null,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.create({
|
||||||
|
name: "second location",
|
||||||
|
metadata: {
|
||||||
|
extra: "abc",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
address_1: "addr_1",
|
||||||
|
address_2: "line 2",
|
||||||
|
country_code: "DK",
|
||||||
|
city: "city",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: "second location",
|
||||||
|
metadata: {
|
||||||
|
extra: "abc",
|
||||||
|
},
|
||||||
|
address_id: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("update", async () => {
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
|
||||||
|
const loc = await stockLocationService.create({
|
||||||
|
name: "location",
|
||||||
|
address: {
|
||||||
|
address_1: "addr_1",
|
||||||
|
address_2: "line 2",
|
||||||
|
country_code: "DK",
|
||||||
|
city: "city",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const addressId = loc.address_id
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.retrieve(loc.id, {
|
||||||
|
relations: ["address"],
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: loc.id,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
deleted_at: null,
|
||||||
|
name: "location",
|
||||||
|
address_id: addressId,
|
||||||
|
metadata: null,
|
||||||
|
address: expect.objectContaining({
|
||||||
|
id: addressId,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
deleted_at: null,
|
||||||
|
address_1: "addr_1",
|
||||||
|
address_2: "line 2",
|
||||||
|
company: null,
|
||||||
|
city: "city",
|
||||||
|
country_code: "DK",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: { abc: 123 },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.update(loc.id, {
|
||||||
|
name: "location name",
|
||||||
|
address_id: addressId,
|
||||||
|
address: {
|
||||||
|
address_1: "addr_1 updated",
|
||||||
|
country_code: "US",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: loc.id,
|
||||||
|
name: "location name",
|
||||||
|
address_id: addressId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.retrieve(loc.id, {
|
||||||
|
relations: ["address"],
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: loc.id,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
deleted_at: null,
|
||||||
|
name: "location name",
|
||||||
|
address_id: addressId,
|
||||||
|
metadata: null,
|
||||||
|
address: expect.objectContaining({
|
||||||
|
id: addressId,
|
||||||
|
created_at: expect.any(Date),
|
||||||
|
updated_at: expect.any(Date),
|
||||||
|
deleted_at: null,
|
||||||
|
address_1: "addr_1 updated",
|
||||||
|
address_2: "line 2",
|
||||||
|
company: null,
|
||||||
|
city: "city",
|
||||||
|
country_code: "US",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: { abc: 123 },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updateAddress", async () => {
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
|
||||||
|
const loc = await stockLocationService.create({
|
||||||
|
name: "location",
|
||||||
|
address: {
|
||||||
|
address_1: "addr_1",
|
||||||
|
address_2: "line 2",
|
||||||
|
country_code: "DK",
|
||||||
|
city: "city",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const addressId = loc.address_id
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.updateAddress(addressId, {
|
||||||
|
address_1: "addr_1 updated",
|
||||||
|
country_code: "US",
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: addressId,
|
||||||
|
address_1: "addr_1 updated",
|
||||||
|
address_2: "line 2",
|
||||||
|
country_code: "US",
|
||||||
|
city: "city",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await stockLocationService.retrieve(loc.id, {
|
||||||
|
relations: ["address"],
|
||||||
|
})
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: loc.id,
|
||||||
|
address_id: addressId,
|
||||||
|
address: expect.objectContaining({
|
||||||
|
id: addressId,
|
||||||
|
address_1: "addr_1 updated",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("delete", async () => {
|
||||||
|
const stockLocationService = appContainer.resolve("stockLocationService")
|
||||||
|
|
||||||
|
const loc = await stockLocationService.create({
|
||||||
|
name: "location",
|
||||||
|
address: {
|
||||||
|
address_1: "addr_1",
|
||||||
|
address_2: "line 2",
|
||||||
|
country_code: "DK",
|
||||||
|
city: "city",
|
||||||
|
phone: "111222333",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "555-714",
|
||||||
|
metadata: {
|
||||||
|
abc: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await stockLocationService.delete(loc.id)
|
||||||
|
|
||||||
|
const deletedItem = stockLocationService.retrieve(loc.id)
|
||||||
|
|
||||||
|
await expect(deletedItem).rejects.toThrow(
|
||||||
|
`StockLocation with id ${loc.id} was not found`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -17,6 +17,8 @@ const {
|
|||||||
} = require("@medusajs/medusa")
|
} = require("@medusajs/medusa")
|
||||||
|
|
||||||
module.exports = async (connection, data = {}) => {
|
module.exports = async (connection, data = {}) => {
|
||||||
|
const salesChannelId = data?.sales_channel_id
|
||||||
|
|
||||||
const yesterday = ((today) => new Date(today.setDate(today.getDate() - 1)))(
|
const yesterday = ((today) => new Date(today.setDate(today.getDate() - 1)))(
|
||||||
new Date()
|
new Date()
|
||||||
)
|
)
|
||||||
@@ -240,7 +242,7 @@ module.exports = async (connection, data = {}) => {
|
|||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
starts_at: tenDaysAgo,
|
starts_at: tenDaysAgo,
|
||||||
ends_at: tenDaysFromToday,
|
ends_at: tenDaysFromToday,
|
||||||
valid_duration: "P1M", //one month
|
valid_duration: "P1M", // one month
|
||||||
})
|
})
|
||||||
|
|
||||||
DynamicDiscount.regions = [r]
|
DynamicDiscount.regions = [r]
|
||||||
@@ -381,6 +383,7 @@ module.exports = async (connection, data = {}) => {
|
|||||||
const cart = manager.create(Cart, {
|
const cart = manager.create(Cart, {
|
||||||
id: "test-cart",
|
id: "test-cart",
|
||||||
customer_id: "some-customer",
|
customer_id: "some-customer",
|
||||||
|
sales_channel_id: salesChannelId,
|
||||||
email: "some-customer@email.com",
|
email: "some-customer@email.com",
|
||||||
shipping_address: {
|
shipping_address: {
|
||||||
id: "test-shipping-address",
|
id: "test-shipping-address",
|
||||||
@@ -397,6 +400,7 @@ module.exports = async (connection, data = {}) => {
|
|||||||
const cart2 = manager.create(Cart, {
|
const cart2 = manager.create(Cart, {
|
||||||
id: "test-cart-2",
|
id: "test-cart-2",
|
||||||
customer_id: "some-customer",
|
customer_id: "some-customer",
|
||||||
|
sales_channel_id: salesChannelId,
|
||||||
email: "some-customer@email.com",
|
email: "some-customer@email.com",
|
||||||
shipping_address: {
|
shipping_address: {
|
||||||
id: "test-shipping-address",
|
id: "test-shipping-address",
|
||||||
@@ -413,6 +417,7 @@ module.exports = async (connection, data = {}) => {
|
|||||||
id: "swap-cart",
|
id: "swap-cart",
|
||||||
type: "swap",
|
type: "swap",
|
||||||
customer_id: "some-customer",
|
customer_id: "some-customer",
|
||||||
|
sales_channel_id: salesChannelId,
|
||||||
email: "some-customer@email.com",
|
email: "some-customer@email.com",
|
||||||
shipping_address: {
|
shipping_address: {
|
||||||
id: "test-shipping-address",
|
id: "test-shipping-address",
|
||||||
|
|||||||
@@ -28,4 +28,16 @@ module.exports = {
|
|||||||
jwt_secret: "test",
|
jwt_secret: "test",
|
||||||
cookie_secret: "test",
|
cookie_secret: "test",
|
||||||
},
|
},
|
||||||
|
modules: {
|
||||||
|
stockLocationService: {
|
||||||
|
scope: "internal",
|
||||||
|
resources: "shared",
|
||||||
|
resolve: "@medusajs/stock-location",
|
||||||
|
},
|
||||||
|
inventoryService: {
|
||||||
|
scope: "internal",
|
||||||
|
resources: "shared",
|
||||||
|
resolve: "@medusajs/inventory",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -223,8 +223,12 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
*/
|
*/
|
||||||
async getStockedQuantity(
|
async getStockedQuantity(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[]
|
locationIds: string[] | string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
if (!Array.isArray(locationIds)) {
|
||||||
|
locationIds = [locationIds]
|
||||||
|
}
|
||||||
|
|
||||||
const manager = this.getManager()
|
const manager = this.getManager()
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
@@ -235,7 +239,7 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
.andWhere("location_id IN (:...locationIds)", { locationIds })
|
.andWhere("location_id IN (:...locationIds)", { locationIds })
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
|
||||||
return result.quantity
|
return parseFloat(result.quantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,8 +250,12 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
*/
|
*/
|
||||||
async getAvailableQuantity(
|
async getAvailableQuantity(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[]
|
locationIds: string[] | string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
if (!Array.isArray(locationIds)) {
|
||||||
|
locationIds = [locationIds]
|
||||||
|
}
|
||||||
|
|
||||||
const manager = this.getManager()
|
const manager = this.getManager()
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
@@ -258,7 +266,7 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
.andWhere("location_id IN (:...locationIds)", { locationIds })
|
.andWhere("location_id IN (:...locationIds)", { locationIds })
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
|
||||||
return result.quantity
|
return parseFloat(result.quantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,8 +277,12 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
*/
|
*/
|
||||||
async getReservedQuantity(
|
async getReservedQuantity(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[]
|
locationIds: string[] | string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
if (!Array.isArray(locationIds)) {
|
||||||
|
locationIds = [locationIds]
|
||||||
|
}
|
||||||
|
|
||||||
const manager = this.getManager()
|
const manager = this.getManager()
|
||||||
const levelRepository = manager.getRepository(InventoryLevel)
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
@@ -281,6 +293,6 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
.andWhere("location_id IN (:...locationIds)", { locationIds })
|
.andWhere("location_id IN (:...locationIds)", { locationIds })
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
|
||||||
return result.quantity
|
return parseFloat(result.quantity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,8 +173,31 @@ export default class ReservationItemService extends TransactionBaseService {
|
|||||||
const shouldUpdateQuantity =
|
const shouldUpdateQuantity =
|
||||||
isDefined(data.quantity) && data.quantity !== item.quantity
|
isDefined(data.quantity) && data.quantity !== item.quantity
|
||||||
|
|
||||||
|
const shouldUpdateLocation =
|
||||||
|
isDefined(data.location_id) &&
|
||||||
|
isDefined(data.quantity) &&
|
||||||
|
data.location_id !== item.location_id
|
||||||
|
|
||||||
const ops: Promise<unknown>[] = []
|
const ops: Promise<unknown>[] = []
|
||||||
if (shouldUpdateQuantity) {
|
|
||||||
|
if (shouldUpdateLocation) {
|
||||||
|
ops.push(
|
||||||
|
this.inventoryLevelService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.adjustReservedQuantity(
|
||||||
|
item.inventory_item_id,
|
||||||
|
item.location_id,
|
||||||
|
item.quantity * -1
|
||||||
|
),
|
||||||
|
this.inventoryLevelService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.adjustReservedQuantity(
|
||||||
|
item.inventory_item_id,
|
||||||
|
data.location_id!,
|
||||||
|
data.quantity!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (shouldUpdateQuantity) {
|
||||||
const quantityDiff = data.quantity! - item.quantity
|
const quantityDiff = data.quantity! - item.quantity
|
||||||
ops.push(
|
ops.push(
|
||||||
this.inventoryLevelService_
|
this.inventoryLevelService_
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export const buildLevelsByInventoryItemId = (
|
|||||||
inventoryLevels: InventoryLevelDTO[],
|
inventoryLevels: InventoryLevelDTO[],
|
||||||
locationIds: string[]
|
locationIds: string[]
|
||||||
) => {
|
) => {
|
||||||
const filteredLevels = inventoryLevels.filter((level) =>
|
const filteredLevels = inventoryLevels.filter((level) => {
|
||||||
locationIds?.includes(level.location_id)
|
return !locationIds.length || locationIds.includes(level.location_id)
|
||||||
)
|
})
|
||||||
|
|
||||||
return filteredLevels.reduce((acc, level) => {
|
return filteredLevels.reduce((acc, level) => {
|
||||||
acc[level.inventory_item_id] = acc[level.inventory_item_id] ?? []
|
acc[level.inventory_item_id] = acc[level.inventory_item_id] ?? []
|
||||||
@@ -58,6 +58,7 @@ export const joinLevels = async (
|
|||||||
locationIds,
|
locationIds,
|
||||||
inventoryService
|
inventoryService
|
||||||
)
|
)
|
||||||
|
|
||||||
return inventoryItems.map((inventoryItem) => ({
|
return inventoryItems.map((inventoryItem) => ({
|
||||||
...inventoryItem,
|
...inventoryItem,
|
||||||
location_levels: levelsByItemId[inventoryItem.id] || [],
|
location_levels: levelsByItemId[inventoryItem.id] || [],
|
||||||
|
|||||||
@@ -82,12 +82,7 @@ export const createVariantTransaction = async (
|
|||||||
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
||||||
|
|
||||||
async function createVariant(variantInput: CreateProductVariantInput) {
|
async function createVariant(variantInput: CreateProductVariantInput) {
|
||||||
const variant = await productVariantServiceTx.create(
|
return await productVariantServiceTx.create(productId, variantInput)
|
||||||
productId,
|
|
||||||
variantInput
|
|
||||||
)
|
|
||||||
|
|
||||||
return { variant }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeVariant(variant: ProductVariant) {
|
async function removeVariant(variant: ProductVariant) {
|
||||||
@@ -101,7 +96,7 @@ export const createVariantTransaction = async (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const inventoryItem = await inventoryServiceTx!.createInventoryItem({
|
return await inventoryServiceTx!.createInventoryItem({
|
||||||
sku: variant.sku,
|
sku: variant.sku,
|
||||||
origin_country: variant.origin_country,
|
origin_country: variant.origin_country,
|
||||||
hs_code: variant.hs_code,
|
hs_code: variant.hs_code,
|
||||||
@@ -112,8 +107,6 @@ export const createVariantTransaction = async (
|
|||||||
height: variant.height,
|
height: variant.height,
|
||||||
width: variant.width,
|
width: variant.width,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { inventoryItem }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
|
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
export const InventoryServiceMock = {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
listInventoryItems: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
listReservationItems: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
listInventoryLevels: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
retrieveInventoryItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
retrieveInventoryLevel: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
createReservationItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
createInventoryItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
createInventoryLevel: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateInventoryLevel: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateInventoryItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateReservationItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteReservationItemsByLineItem: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteReservationItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteInventoryItem: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteInventoryLevel: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
adjustInventory: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
confirmInventory: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
retrieveAvailableQuantity: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve(10)
|
||||||
|
}),
|
||||||
|
|
||||||
|
retrieveStockedQuantity: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve(9)
|
||||||
|
}),
|
||||||
|
|
||||||
|
retrieveReservedQuantity: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve(1)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return InventoryServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const StockLocationServiceMock = {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
retrieve: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
id: id,
|
||||||
|
name: "stock location 1 name",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
update: jest.fn().mockImplementation((id, data) => {
|
||||||
|
return Promise.resolve({ id, ...data })
|
||||||
|
}),
|
||||||
|
|
||||||
|
listAndCount: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: IdMap.getId("stock_location_1"),
|
||||||
|
name: "stock location 1 name",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
|
||||||
|
create: jest.fn().mockImplementation((data) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
id: id,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: jest.fn().mockImplementation((id, config) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return StockLocationServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -69,11 +69,17 @@ class SalesChannelLocationService extends TransactionBaseService {
|
|||||||
const salesChannel = await this.salesChannelService_
|
const salesChannel = await this.salesChannelService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.retrieve(salesChannelId)
|
.retrieve(salesChannelId)
|
||||||
|
|
||||||
|
const stockLocationId = locationId
|
||||||
|
|
||||||
|
if (this.stockLocationService) {
|
||||||
const stockLocation = await this.stockLocationService.retrieve(locationId)
|
const stockLocation = await this.stockLocationService.retrieve(locationId)
|
||||||
|
locationId = stockLocation.id
|
||||||
|
}
|
||||||
|
|
||||||
const salesChannelLocation = manager.create(SalesChannelLocation, {
|
const salesChannelLocation = manager.create(SalesChannelLocation, {
|
||||||
sales_channel_id: salesChannel.id,
|
sales_channel_id: salesChannel.id,
|
||||||
location_id: stockLocation.id,
|
location_id: stockLocationId,
|
||||||
})
|
})
|
||||||
|
|
||||||
await manager.save(salesChannelLocation)
|
await manager.save(salesChannelLocation)
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ export type CreateInventoryItemInput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CreateReservationItemInput = {
|
export type CreateReservationItemInput = {
|
||||||
type?: string
|
|
||||||
line_item_id?: string
|
line_item_id?: string
|
||||||
inventory_item_id: string
|
inventory_item_id: string
|
||||||
location_id: string
|
location_id: string
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export default class StockLocationService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { metadata, ...fields } = updateData
|
const { metadata, ...fields } = data
|
||||||
|
|
||||||
const toSave = locationRepo.merge(item, fields)
|
const toSave = locationRepo.merge(item, fields)
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
|
|||||||
Reference in new issue
Block a user