Feat/fulfill swaps and claims with locations (#3518)
* re-add if statements * initial implementation * add changeset * address feedback * remove context param from create-fulfillment * fix tests
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Feat(medusa): fulfill swaps and claims with locations
|
||||||
@@ -207,7 +207,7 @@ describe("Inventory Items endpoints", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.only("fails to update location level to negative quantity", async () => {
|
it("fails to update location level to negative quantity", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const inventoryItemId = inventoryItems[0].id
|
const inventoryItemId = inventoryItems[0].id
|
||||||
|
|||||||
@@ -172,10 +172,6 @@ const CreateFulfillmentModal: React.FC<CreateFulfillmentModalProps> = ({
|
|||||||
no_notification: noNotis,
|
no_notification: noNotis,
|
||||||
} as AdminPostOrdersOrderFulfillmentsReq
|
} as AdminPostOrdersOrderFulfillmentsReq
|
||||||
|
|
||||||
if (isLocationFulfillmentEnabled) {
|
|
||||||
requestObj.location_id = locationSelectValue.value
|
|
||||||
}
|
|
||||||
|
|
||||||
requestObj.items = Object.entries(quantities)
|
requestObj.items = Object.entries(quantities)
|
||||||
.filter(([, value]) => !!value)
|
.filter(([, value]) => !!value)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
@@ -185,6 +181,10 @@ const CreateFulfillmentModal: React.FC<CreateFulfillmentModalProps> = ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLocationFulfillmentEnabled) {
|
||||||
|
requestObj.location_id = locationSelectValue.value
|
||||||
|
}
|
||||||
|
|
||||||
action.mutate(requestObj, {
|
action.mutate(requestObj, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notification("Success", successText, "success")
|
notification("Success", successText, "success")
|
||||||
|
|||||||
@@ -474,6 +474,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (locationIds.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
const availableQuantity =
|
const availableQuantity =
|
||||||
await this.inventoryLevelService_.getAvailableQuantity(
|
await this.inventoryLevelService_.getAvailableQuantity(
|
||||||
inventoryItemId,
|
inventoryItemId,
|
||||||
@@ -506,6 +510,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (locationIds.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
const stockedQuantity =
|
const stockedQuantity =
|
||||||
await this.inventoryLevelService_.getStockedQuantity(
|
await this.inventoryLevelService_.getStockedQuantity(
|
||||||
inventoryItemId,
|
inventoryItemId,
|
||||||
@@ -538,6 +546,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (locationIds.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
const reservedQuantity =
|
const reservedQuantity =
|
||||||
await this.inventoryLevelService_.getReservedQuantity(
|
await this.inventoryLevelService_.getReservedQuantity(
|
||||||
inventoryItemId,
|
inventoryItemId,
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default async (req, res) => {
|
|||||||
res.json({ order })
|
res.json({ order })
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateInventoryAndReservations = async (
|
export const updateInventoryAndReservations = async (
|
||||||
fulfillments: Fulfillment[],
|
fulfillments: Fulfillment[],
|
||||||
context: {
|
context: {
|
||||||
inventoryService: ProductVariantInventoryService
|
inventoryService: ProductVariantInventoryService
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { ClaimService, OrderService } from "../../../../services"
|
import {
|
||||||
import { IsBoolean, IsObject, IsOptional } from "class-validator"
|
ClaimService,
|
||||||
|
OrderService,
|
||||||
|
ProductVariantInventoryService,
|
||||||
|
} from "../../../../services"
|
||||||
|
import { IsBoolean, IsObject, IsOptional, IsString } from "class-validator"
|
||||||
|
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { FindParams } from "../../../../types/common"
|
import { FindParams } from "../../../../types/common"
|
||||||
|
import { updateInventoryAndReservations } from "./create-fulfillment"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /admin/orders/{id}/claims/{claim_id}/fulfillments
|
* @oas [post] /admin/orders/{id}/claims/{claim_id}/fulfillments
|
||||||
@@ -72,12 +77,51 @@ export default async (req, res) => {
|
|||||||
const orderService: OrderService = req.scope.resolve("orderService")
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
const claimService: ClaimService = req.scope.resolve("claimService")
|
const claimService: ClaimService = req.scope.resolve("claimService")
|
||||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||||
|
const pvInventoryService: ProductVariantInventoryService = req.scope.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
await entityManager.transaction(async (manager) => {
|
await entityManager.transaction(async (manager) => {
|
||||||
await claimService.withTransaction(manager).createFulfillment(claim_id, {
|
const claimServiceTx = claimService.withTransaction(manager)
|
||||||
|
|
||||||
|
const { fulfillments: existingFulfillments } =
|
||||||
|
await claimServiceTx.retrieve(claim_id, {
|
||||||
|
relations: [
|
||||||
|
"fulfillments",
|
||||||
|
"fulfillments.items",
|
||||||
|
"fulfillments.items.item",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingFulfillmentSet = new Set(
|
||||||
|
existingFulfillments.map((fulfillment) => fulfillment.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await claimServiceTx.createFulfillment(claim_id, {
|
||||||
metadata: validated.metadata,
|
metadata: validated.metadata,
|
||||||
no_notification: validated.no_notification,
|
no_notification: validated.no_notification,
|
||||||
|
location_id: validated.location_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (validated.location_id) {
|
||||||
|
const { fulfillments } = await claimServiceTx.retrieve(claim_id, {
|
||||||
|
relations: [
|
||||||
|
"fulfillments",
|
||||||
|
"fulfillments.items",
|
||||||
|
"fulfillments.items.item",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const pvInventoryServiceTx = pvInventoryService.withTransaction(manager)
|
||||||
|
|
||||||
|
await updateInventoryAndReservations(
|
||||||
|
fulfillments.filter((f) => !existingFulfillmentSet.has(f.id)),
|
||||||
|
{
|
||||||
|
inventoryService: pvInventoryServiceTx,
|
||||||
|
locationId: validated.location_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const order = await orderService.retrieveWithTotals(id, req.retrieveConfig, {
|
const order = await orderService.retrieveWithTotals(id, req.retrieveConfig, {
|
||||||
@@ -106,6 +150,10 @@ export class AdminPostOrdersOrderClaimsClaimFulfillmentsReq {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
location_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { IsBoolean, IsObject, IsOptional } from "class-validator"
|
import { IsBoolean, IsObject, IsOptional, IsString } from "class-validator"
|
||||||
import { OrderService, SwapService } from "../../../../services"
|
import {
|
||||||
|
OrderService,
|
||||||
|
ProductVariantInventoryService,
|
||||||
|
SwapService,
|
||||||
|
} from "../../../../services"
|
||||||
|
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import { FindParams } from "../../../../types/common"
|
import { FindParams } from "../../../../types/common"
|
||||||
|
import { updateInventoryAndReservations } from "./create-fulfillment"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /admin/orders/{id}/swaps/{swap_id}/fulfillments
|
* @oas [post] /admin/orders/{id}/swaps/{swap_id}/fulfillments
|
||||||
@@ -76,12 +81,53 @@ export default async (req, res) => {
|
|||||||
const orderService: OrderService = req.scope.resolve("orderService")
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
const swapService: SwapService = req.scope.resolve("swapService")
|
const swapService: SwapService = req.scope.resolve("swapService")
|
||||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||||
|
const pvInventoryService: ProductVariantInventoryService = req.scope.resolve(
|
||||||
|
"productVariantInventoryService"
|
||||||
|
)
|
||||||
|
|
||||||
await entityManager.transaction(async (manager) => {
|
await entityManager.transaction(async (manager) => {
|
||||||
await swapService.withTransaction(manager).createFulfillment(swap_id, {
|
const swapServiceTx = swapService.withTransaction(manager)
|
||||||
|
|
||||||
|
const { fulfillments: existingFulfillments } = await swapServiceTx.retrieve(
|
||||||
|
swap_id,
|
||||||
|
{
|
||||||
|
relations: [
|
||||||
|
"fulfillments",
|
||||||
|
"fulfillments.items",
|
||||||
|
"fulfillments.items.item",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const existingFulfillmentSet = new Set(
|
||||||
|
existingFulfillments.map((fulfillment) => fulfillment.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await swapServiceTx.createFulfillment(swap_id, {
|
||||||
metadata: validated.metadata,
|
metadata: validated.metadata,
|
||||||
no_notification: validated.no_notification,
|
no_notification: validated.no_notification,
|
||||||
|
location_id: validated.location_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (validated.location_id) {
|
||||||
|
const { fulfillments } = await swapServiceTx.retrieve(swap_id, {
|
||||||
|
relations: [
|
||||||
|
"fulfillments",
|
||||||
|
"fulfillments.items",
|
||||||
|
"fulfillments.items.item",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const pvInventoryServiceTx = pvInventoryService.withTransaction(manager)
|
||||||
|
|
||||||
|
await updateInventoryAndReservations(
|
||||||
|
fulfillments.filter((f) => !existingFulfillmentSet.has(f.id)),
|
||||||
|
{
|
||||||
|
inventoryService: pvInventoryServiceTx,
|
||||||
|
locationId: validated.location_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const order = await orderService.retrieveWithTotals(id, req.retrieveConfig, {
|
const order = await orderService.retrieveWithTotals(id, req.retrieveConfig, {
|
||||||
@@ -110,6 +156,10 @@ export class AdminPostOrdersOrderSwapsSwapFulfillmentsReq {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
location_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@@ -510,9 +510,12 @@ describe("OrderService", () => {
|
|||||||
it("calls order model functions", async () => {
|
it("calls order model functions", async () => {
|
||||||
await orderService.retrieve(IdMap.getId("test-order"))
|
await orderService.retrieve(IdMap.getId("test-order"))
|
||||||
expect(orderRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
|
expect(orderRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
|
||||||
expect(orderRepo.findOneWithRelations).toHaveBeenCalledWith({}, {
|
expect(orderRepo.findOneWithRelations).toHaveBeenCalledWith(
|
||||||
where: { id: IdMap.getId("test-order") },
|
{},
|
||||||
})
|
{
|
||||||
|
where: { id: IdMap.getId("test-order") },
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -934,8 +937,7 @@ describe("OrderService", () => {
|
|||||||
quantity: 2,
|
quantity: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ metadata: {}, order_id: "test-order" },
|
{ metadata: {}, order_id: "test-order", location_id: undefined }
|
||||||
{ location_id: undefined }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||||
@@ -967,8 +969,7 @@ describe("OrderService", () => {
|
|||||||
quantity: 2,
|
quantity: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ metadata: {}, order_id: "partial" },
|
{ metadata: {}, order_id: "partial", location_id: undefined }
|
||||||
{ location_id: undefined }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||||
@@ -1000,8 +1001,7 @@ describe("OrderService", () => {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ metadata: {}, order_id: "test" },
|
{ metadata: {}, order_id: "test", location_id: undefined }
|
||||||
{ location_id: undefined }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||||
@@ -1039,8 +1039,12 @@ describe("OrderService", () => {
|
|||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{ metadata: {}, order_id: "test", no_notification: undefined },
|
{
|
||||||
{ locationId: "loc_1" }
|
metadata: {},
|
||||||
|
order_id: "test",
|
||||||
|
no_notification: undefined,
|
||||||
|
location_id: "loc_1",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
config: {
|
config: {
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
|
location_id?: string
|
||||||
} = {
|
} = {
|
||||||
metadata: {},
|
metadata: {},
|
||||||
}
|
}
|
||||||
@@ -595,7 +596,7 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
item_id: i.id,
|
item_id: i.id,
|
||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
})),
|
})),
|
||||||
{ claim_order_id: id, metadata }
|
{ claim_order_id: id, metadata, location_id: config.location_id }
|
||||||
)
|
)
|
||||||
|
|
||||||
let successfullyFulfilledItems: FulfillmentItem[] = []
|
let successfullyFulfilledItems: FulfillmentItem[] = []
|
||||||
|
|||||||
@@ -205,10 +205,8 @@ class FulfillmentService extends TransactionBaseService {
|
|||||||
async createFulfillment(
|
async createFulfillment(
|
||||||
order: CreateFulfillmentOrder,
|
order: CreateFulfillmentOrder,
|
||||||
itemsToFulfill: FulFillmentItemType[],
|
itemsToFulfill: FulFillmentItemType[],
|
||||||
custom: Partial<Fulfillment> = {},
|
custom: Partial<Fulfillment> = {}
|
||||||
context: { locationId?: string } = {}
|
|
||||||
): Promise<Fulfillment[]> {
|
): Promise<Fulfillment[]> {
|
||||||
const { locationId } = context
|
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
const fulfillmentRepository = manager.withRepository(
|
const fulfillmentRepository = manager.withRepository(
|
||||||
this.fulfillmentRepository_
|
this.fulfillmentRepository_
|
||||||
@@ -231,7 +229,6 @@ class FulfillmentService extends TransactionBaseService {
|
|||||||
provider_id: shipping_method.shipping_option.provider_id,
|
provider_id: shipping_method.shipping_option.provider_id,
|
||||||
items: items.map((i) => ({ item_id: i.id, quantity: i.quantity })),
|
items: items.map((i) => ({ item_id: i.id, quantity: i.quantity })),
|
||||||
data: {},
|
data: {},
|
||||||
location_id: locationId,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await fulfillmentRepository.save(ful)
|
const result = await fulfillmentRepository.save(ful)
|
||||||
|
|||||||
@@ -1410,9 +1410,7 @@ class OrderService extends TransactionBaseService {
|
|||||||
metadata: metadata ?? {},
|
metadata: metadata ?? {},
|
||||||
no_notification: no_notification,
|
no_notification: no_notification,
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
},
|
location_id: location_id,
|
||||||
{
|
|
||||||
locationId: location_id,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
let successfullyFulfilled: FulfillmentItem[] = []
|
let successfullyFulfilled: FulfillmentItem[] = []
|
||||||
|
|||||||
@@ -791,7 +791,7 @@ class SwapService extends TransactionBaseService {
|
|||||||
// Is the cascade insert really used? Also, is it really necessary to pass the entire entities when creating or updating?
|
// Is the cascade insert really used? Also, is it really necessary to pass the entire entities when creating or updating?
|
||||||
// We normally should only pass what is needed?
|
// We normally should only pass what is needed?
|
||||||
swap.shipping_methods = cart.shipping_methods.map((method) => {
|
swap.shipping_methods = cart.shipping_methods.map((method) => {
|
||||||
;(method.tax_lines as any) = undefined
|
(method.tax_lines as any) = undefined
|
||||||
return method
|
return method
|
||||||
})
|
})
|
||||||
swap.confirmed_at = new Date()
|
swap.confirmed_at = new Date()
|
||||||
@@ -975,7 +975,7 @@ class SwapService extends TransactionBaseService {
|
|||||||
item_id: i.id,
|
item_id: i.id,
|
||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
})),
|
})),
|
||||||
{ swap_id: swapId, metadata }
|
{ swap_id: swapId, metadata, location_id: config.location_id }
|
||||||
)
|
)
|
||||||
|
|
||||||
let successfullyFulfilled: FulfillmentItem[] = []
|
let successfullyFulfilled: FulfillmentItem[] = []
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type FulfillmentItemPartition = {
|
|||||||
export type CreateShipmentConfig = {
|
export type CreateShipmentConfig = {
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
no_notification?: boolean
|
no_notification?: boolean
|
||||||
|
location_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateFulfillmentOrder = Omit<ClaimOrder, "beforeInsert"> & {
|
export type CreateFulfillmentOrder = Omit<ClaimOrder, "beforeInsert"> & {
|
||||||
|
|||||||
Reference in New Issue
Block a user