From 026bdab05d4da054d3ffd07b8cce8ccb1bded95d Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 20 Mar 2023 13:48:25 +0100 Subject: [PATCH] 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 --- .changeset/wild-goats-sing.md | 5 ++ .../inventory/inventory-items/index.js | 2 +- .../details/create-fulfillment/index.tsx | 8 +-- packages/inventory/src/services/inventory.ts | 12 ++++ .../routes/admin/orders/create-fulfillment.ts | 2 +- .../api/routes/admin/orders/fulfill-claim.ts | 54 +++++++++++++++++- .../api/routes/admin/orders/fulfill-swap.ts | 56 ++++++++++++++++++- .../medusa/src/services/__tests__/order.js | 26 +++++---- packages/medusa/src/services/claim.ts | 3 +- packages/medusa/src/services/fulfillment.ts | 5 +- packages/medusa/src/services/order.ts | 4 +- packages/medusa/src/services/swap.ts | 4 +- packages/medusa/src/types/fulfillment.ts | 1 + 13 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 .changeset/wild-goats-sing.md diff --git a/.changeset/wild-goats-sing.md b/.changeset/wild-goats-sing.md new file mode 100644 index 0000000000..eee8b6e26e --- /dev/null +++ b/.changeset/wild-goats-sing.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Feat(medusa): fulfill swaps and claims with locations diff --git a/integration-tests/plugins/__tests__/inventory/inventory-items/index.js b/integration-tests/plugins/__tests__/inventory/inventory-items/index.js index 96a0e8044e..b9ace5f7b2 100644 --- a/integration-tests/plugins/__tests__/inventory/inventory-items/index.js +++ b/integration-tests/plugins/__tests__/inventory/inventory-items/index.js @@ -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 inventoryItemId = inventoryItems[0].id diff --git a/packages/admin-ui/ui/src/domain/orders/details/create-fulfillment/index.tsx b/packages/admin-ui/ui/src/domain/orders/details/create-fulfillment/index.tsx index 66f0e44671..33cf745de7 100644 --- a/packages/admin-ui/ui/src/domain/orders/details/create-fulfillment/index.tsx +++ b/packages/admin-ui/ui/src/domain/orders/details/create-fulfillment/index.tsx @@ -172,10 +172,6 @@ const CreateFulfillmentModal: React.FC = ({ no_notification: noNotis, } as AdminPostOrdersOrderFulfillmentsReq - if (isLocationFulfillmentEnabled) { - requestObj.location_id = locationSelectValue.value - } - requestObj.items = Object.entries(quantities) .filter(([, value]) => !!value) .map(([key, value]) => ({ @@ -185,6 +181,10 @@ const CreateFulfillmentModal: React.FC = ({ break } + if (isLocationFulfillmentEnabled) { + requestObj.location_id = locationSelectValue.value + } + action.mutate(requestObj, { onSuccess: () => { notification("Success", successText, "success") diff --git a/packages/inventory/src/services/inventory.ts b/packages/inventory/src/services/inventory.ts index a8629073a1..b366d898fc 100644 --- a/packages/inventory/src/services/inventory.ts +++ b/packages/inventory/src/services/inventory.ts @@ -474,6 +474,10 @@ export default class InventoryService implements IInventoryService { context ) + if (locationIds.length === 0) { + return 0 + } + const availableQuantity = await this.inventoryLevelService_.getAvailableQuantity( inventoryItemId, @@ -506,6 +510,10 @@ export default class InventoryService implements IInventoryService { context ) + if (locationIds.length === 0) { + return 0 + } + const stockedQuantity = await this.inventoryLevelService_.getStockedQuantity( inventoryItemId, @@ -538,6 +546,10 @@ export default class InventoryService implements IInventoryService { context ) + if (locationIds.length === 0) { + return 0 + } + const reservedQuantity = await this.inventoryLevelService_.getReservedQuantity( inventoryItemId, diff --git a/packages/medusa/src/api/routes/admin/orders/create-fulfillment.ts b/packages/medusa/src/api/routes/admin/orders/create-fulfillment.ts index 2c10ed6dbd..27d2063244 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-fulfillment.ts +++ b/packages/medusa/src/api/routes/admin/orders/create-fulfillment.ts @@ -153,7 +153,7 @@ export default async (req, res) => { res.json({ order }) } -const updateInventoryAndReservations = async ( +export const updateInventoryAndReservations = async ( fulfillments: Fulfillment[], context: { inventoryService: ProductVariantInventoryService diff --git a/packages/medusa/src/api/routes/admin/orders/fulfill-claim.ts b/packages/medusa/src/api/routes/admin/orders/fulfill-claim.ts index 852f548082..43e8b83f96 100644 --- a/packages/medusa/src/api/routes/admin/orders/fulfill-claim.ts +++ b/packages/medusa/src/api/routes/admin/orders/fulfill-claim.ts @@ -1,8 +1,13 @@ -import { ClaimService, OrderService } from "../../../../services" -import { IsBoolean, IsObject, IsOptional } from "class-validator" +import { + ClaimService, + OrderService, + ProductVariantInventoryService, +} from "../../../../services" +import { IsBoolean, IsObject, IsOptional, IsString } from "class-validator" import { EntityManager } from "typeorm" import { FindParams } from "../../../../types/common" +import { updateInventoryAndReservations } from "./create-fulfillment" /** * @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 claimService: ClaimService = req.scope.resolve("claimService") const entityManager: EntityManager = req.scope.resolve("manager") + const pvInventoryService: ProductVariantInventoryService = req.scope.resolve( + "productVariantInventoryService" + ) 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, 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, { @@ -106,6 +150,10 @@ export class AdminPostOrdersOrderClaimsClaimFulfillmentsReq { @IsBoolean() @IsOptional() no_notification?: boolean + + @IsString() + @IsOptional() + location_id?: string } // eslint-disable-next-line max-len diff --git a/packages/medusa/src/api/routes/admin/orders/fulfill-swap.ts b/packages/medusa/src/api/routes/admin/orders/fulfill-swap.ts index 85642c54ed..7d93afb21e 100644 --- a/packages/medusa/src/api/routes/admin/orders/fulfill-swap.ts +++ b/packages/medusa/src/api/routes/admin/orders/fulfill-swap.ts @@ -1,9 +1,14 @@ -import { IsBoolean, IsObject, IsOptional } from "class-validator" -import { OrderService, SwapService } from "../../../../services" +import { IsBoolean, IsObject, IsOptional, IsString } from "class-validator" +import { + OrderService, + ProductVariantInventoryService, + SwapService, +} from "../../../../services" import { EntityManager } from "typeorm" import { validator } from "../../../../utils/validator" import { FindParams } from "../../../../types/common" +import { updateInventoryAndReservations } from "./create-fulfillment" /** * @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 swapService: SwapService = req.scope.resolve("swapService") const entityManager: EntityManager = req.scope.resolve("manager") + const pvInventoryService: ProductVariantInventoryService = req.scope.resolve( + "productVariantInventoryService" + ) 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, 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, { @@ -110,6 +156,10 @@ export class AdminPostOrdersOrderSwapsSwapFulfillmentsReq { @IsBoolean() @IsOptional() no_notification?: boolean + + @IsString() + @IsOptional() + location_id?: string } // eslint-disable-next-line max-len diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index 49b6e32ccb..e23460eb3c 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -510,9 +510,12 @@ describe("OrderService", () => { it("calls order model functions", async () => { await orderService.retrieve(IdMap.getId("test-order")) expect(orderRepo.findOneWithRelations).toHaveBeenCalledTimes(1) - expect(orderRepo.findOneWithRelations).toHaveBeenCalledWith({}, { - where: { id: IdMap.getId("test-order") }, - }) + expect(orderRepo.findOneWithRelations).toHaveBeenCalledWith( + {}, + { + where: { id: IdMap.getId("test-order") }, + } + ) }) }) @@ -934,8 +937,7 @@ describe("OrderService", () => { quantity: 2, }, ], - { metadata: {}, order_id: "test-order" }, - { location_id: undefined } + { metadata: {}, order_id: "test-order", location_id: undefined } ) expect(lineItemService.update).toHaveBeenCalledTimes(1) @@ -967,8 +969,7 @@ describe("OrderService", () => { quantity: 2, }, ], - { metadata: {}, order_id: "partial" }, - { location_id: undefined } + { metadata: {}, order_id: "partial", location_id: undefined } ) expect(lineItemService.update).toHaveBeenCalledTimes(1) @@ -1000,8 +1001,7 @@ describe("OrderService", () => { quantity: 1, }, ], - { metadata: {}, order_id: "test" }, - { location_id: undefined } + { metadata: {}, order_id: "test", location_id: undefined } ) expect(lineItemService.update).toHaveBeenCalledTimes(1) @@ -1039,8 +1039,12 @@ describe("OrderService", () => { quantity: 1, }, ], - { metadata: {}, order_id: "test", no_notification: undefined }, - { locationId: "loc_1" } + { + metadata: {}, + order_id: "test", + no_notification: undefined, + location_id: "loc_1", + } ) }) diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index 3c605cc54b..42f382fe9b 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -512,6 +512,7 @@ export default class ClaimService extends TransactionBaseService { config: { metadata?: Record no_notification?: boolean + location_id?: string } = { metadata: {}, } @@ -595,7 +596,7 @@ export default class ClaimService extends TransactionBaseService { item_id: i.id, quantity: i.quantity, })), - { claim_order_id: id, metadata } + { claim_order_id: id, metadata, location_id: config.location_id } ) let successfullyFulfilledItems: FulfillmentItem[] = [] diff --git a/packages/medusa/src/services/fulfillment.ts b/packages/medusa/src/services/fulfillment.ts index 964b59aa02..1b51dc2986 100644 --- a/packages/medusa/src/services/fulfillment.ts +++ b/packages/medusa/src/services/fulfillment.ts @@ -205,10 +205,8 @@ class FulfillmentService extends TransactionBaseService { async createFulfillment( order: CreateFulfillmentOrder, itemsToFulfill: FulFillmentItemType[], - custom: Partial = {}, - context: { locationId?: string } = {} + custom: Partial = {} ): Promise { - const { locationId } = context return await this.atomicPhase_(async (manager) => { const fulfillmentRepository = manager.withRepository( this.fulfillmentRepository_ @@ -231,7 +229,6 @@ class FulfillmentService extends TransactionBaseService { provider_id: shipping_method.shipping_option.provider_id, items: items.map((i) => ({ item_id: i.id, quantity: i.quantity })), data: {}, - location_id: locationId, }) const result = await fulfillmentRepository.save(ful) diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 6f2a45f451..80fb8b5237 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -1410,9 +1410,7 @@ class OrderService extends TransactionBaseService { metadata: metadata ?? {}, no_notification: no_notification, order_id: orderId, - }, - { - locationId: location_id, + location_id: location_id, } ) let successfullyFulfilled: FulfillmentItem[] = [] diff --git a/packages/medusa/src/services/swap.ts b/packages/medusa/src/services/swap.ts index 25529ad36b..5fc02ff150 100644 --- a/packages/medusa/src/services/swap.ts +++ b/packages/medusa/src/services/swap.ts @@ -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? // We normally should only pass what is needed? swap.shipping_methods = cart.shipping_methods.map((method) => { - ;(method.tax_lines as any) = undefined + (method.tax_lines as any) = undefined return method }) swap.confirmed_at = new Date() @@ -975,7 +975,7 @@ class SwapService extends TransactionBaseService { item_id: i.id, quantity: i.quantity, })), - { swap_id: swapId, metadata } + { swap_id: swapId, metadata, location_id: config.location_id } ) let successfullyFulfilled: FulfillmentItem[] = [] diff --git a/packages/medusa/src/types/fulfillment.ts b/packages/medusa/src/types/fulfillment.ts index 3d8f3d78a2..1f4d0f5330 100644 --- a/packages/medusa/src/types/fulfillment.ts +++ b/packages/medusa/src/types/fulfillment.ts @@ -21,6 +21,7 @@ export type FulfillmentItemPartition = { export type CreateShipmentConfig = { metadata?: Record no_notification?: boolean + location_id?: string } export type CreateFulfillmentOrder = Omit & {