feat(medusa): Create fulfillment with location (#2931)

* remove duplicate key from oas

* changeset

* initial suggestion for adding locations to fulfillments

* update migration

* re-add functionality for removing entire reservations

* fix tests

* add location when adjusting reserved inventory of a line_item

* add changest

* handle multiple reservations for a product in the same channel

* confirm inventory in stock location previous to creating the fulfillment

* fix tests after updating create-fulfillment to confirm inventory prior to creating fulfillment

* remove bugged code

* initial validation

* initial changes for review

* chekcpoint

* update validate inventory at location

* redo some unwanted changes

* typing

* update snapshots

* redo change for eslintrc

* add eslint disable

* re-order methods in interface

* assert no_notification

* iterate one time less

* add test for validation of correct inventory adjustments in case of no inventory service installation

* ensure correct adjustments for order cancellations

* remove comment

* fix tests

* fix but with coalescing

* remove location id from confirm inventory

* don't throw when adjusting reservations for a line item without reservations

* move reservation adjustments to the api

* add multiplication for updating a reservation quantity

* move inventory adjustments from the service layer to the api

* delete reservation if quantity is adjusted to 0

* rename updateReservation to updateReservationItem

* update dto fields

* reference the correct fields

* update with transaction

* add jsdocs

* force boolean cast

* context-ize cancel and create fulfillment transaction methods

* undo notification cast

* update with changes

* refactor withTransaction to variable

* use maps

* fix service mocks
This commit is contained in:
Philip Korsholm
2023-01-09 14:44:34 +01:00
committed by GitHub
parent 28bec599ae
commit 16716f5a4f
16 changed files with 546 additions and 86 deletions
@@ -1,8 +1,13 @@
import { FulfillmentService, OrderService } from "../../../../services"
import {
FulfillmentService,
OrderService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultAdminOrdersFields, defaultAdminOrdersRelations } from "."
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Fulfillment } from "../../../../models"
/**
* @oas [post] /orders/{id}/fulfillments/{fulfillment_id}/cancel
@@ -61,6 +66,9 @@ export default async (req, res) => {
const { id, fulfillment_id } = req.params
const orderService: OrderService = req.scope.resolve("orderService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const fulfillmentService: FulfillmentService =
req.scope.resolve("fulfillmentService")
@@ -75,9 +83,18 @@ export default async (req, res) => {
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await orderService
await orderService
.withTransaction(transactionManager)
.cancelFulfillment(fulfillment_id)
const fulfillment = await fulfillmentService
.withTransaction(transactionManager)
.retrieve(fulfillment_id, { relations: ["items", "items.item"] })
await adjustInventoryForCancelledFulfillment(fulfillment, {
productVariantInventoryService:
productVariantInventoryService.withTransaction(transactionManager),
})
})
const order = await orderService.retrieve(id, {
@@ -87,3 +104,23 @@ export default async (req, res) => {
res.json({ order })
}
export const adjustInventoryForCancelledFulfillment = async (
fulfillment: Fulfillment,
context: {
productVariantInventoryService: ProductVariantInventoryService
}
) => {
const { productVariantInventoryService } = context
await Promise.all(
fulfillment.items.map(async ({ item, quantity }) => {
if (item.variant_id) {
await productVariantInventoryService.adjustInventory(
item.variant_id,
fulfillment.location_id!,
quantity
)
}
})
)
}
@@ -12,9 +12,13 @@ import { Transform, Type } from "class-transformer"
import { defaultAdminOrdersFields, defaultAdminOrdersRelations } from "."
import { EntityManager } from "typeorm"
import { OrderService } from "../../../../services"
import {
OrderService,
ProductVariantInventoryService,
} from "../../../../services"
import { validator } from "../../../../utils/validator"
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
import { Fulfillment, LineItem } from "../../../../models"
/**
* @oas [post] /orders/{id}/fulfillment
@@ -98,15 +102,39 @@ export default async (req, res) => {
)
const orderService: OrderService = req.scope.resolve("orderService")
const pvInventoryService: ProductVariantInventoryService = req.scope.resolve(
"productVariantInventoryService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await orderService
const { fulfillments: existingFulfillments } = await orderService
.withTransaction(transactionManager)
.retrieve(id, {
relations: ["fulfillments"],
})
const existingFulfillmentMap = new Map(
existingFulfillments.map((fulfillment) => [fulfillment.id, fulfillment])
)
const { fulfillments } = await orderService
.withTransaction(transactionManager)
.createFulfillment(id, validated.items, {
metadata: validated.metadata,
no_notification: validated.no_notification,
})
const pvInventoryServiceTx =
pvInventoryService.withTransaction(transactionManager)
if (validated.location_id) {
await updateInventoryAndReservations(
fulfillments.filter((f) => !existingFulfillmentMap[f.id]),
{
inventoryService: pvInventoryServiceTx,
locationId: validated.location_id,
}
)
}
})
const order = await orderService.retrieve(id, {
@@ -117,6 +145,44 @@ export default async (req, res) => {
res.json({ order })
}
const updateInventoryAndReservations = async (
fulfillments: Fulfillment[],
context: {
inventoryService: ProductVariantInventoryService
locationId: string
}
) => {
const { inventoryService, locationId } = context
fulfillments.map(async ({ items }) => {
await inventoryService.validateInventoryAtLocation(
items.map(({ item, quantity }) => ({ ...item, quantity } as LineItem)),
locationId
)
await Promise.all(
items.map(async ({ item, quantity }) => {
if (!item.variant_id) {
return
}
await inventoryService.adjustReservationsQuantityByLineItem(
item.id,
item.variant_id,
locationId,
-quantity
)
await inventoryService.adjustInventory(
item.variant_id,
locationId,
-quantity
)
})
)
})
}
/**
* @schema AdminPostOrdersOrderFulfillmentsReq
* type: object
@@ -150,6 +216,10 @@ export class AdminPostOrdersOrderFulfillmentsReq {
@Type(() => Item)
items: Item[]
@IsString()
@IsOptional()
location_id?: string
@IsBoolean()
@IsOptional()
@Transform(({ value }) => optionalBooleanMapper.get(value))