chore: added missing withTransacton, create-variant using TO (#3047)

Create variant integrated with Inventory modules
This commit is contained in:
Carlos R. L. Rodrigues
2023-01-18 09:29:06 -03:00
committed by GitHub
parent 150696de99
commit aa54d902e5
11 changed files with 409 additions and 128 deletions

View File

@@ -39,6 +39,7 @@ describe("POST /admin/products/:id/variants", () => {
IdMap.getId("productWithOptions"),
{
inventory_quantity: 0,
manage_inventory: true,
title: "Test Product Variant",
options: [],
prices: [

View File

@@ -7,17 +7,33 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import { ProductService, ProductVariantService } from "../../../../services"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import {
ProductService,
ProductVariantService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import { IInventoryService } from "../../../../interfaces"
import {
CreateProductVariantInput,
ProductVariantPricesCreateReq,
} from "../../../../types/product-variant"
import { validator } from "../../../../utils/validator"
import {
TransactionHandlerType,
TransactionOrchestrator,
TransactionPayload,
TransactionState,
TransactionStepsDefinition,
} from "../../../../utils/transaction"
import { ulid } from "ulid"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
/**
* @oas [post] /products/{id}/variants
* operationId: "PostProductsProductVariants"
@@ -103,6 +119,48 @@ import { validator } from "../../../../utils/validator"
* "500":
* $ref: "#/components/responses/500_error"
*/
enum actions {
createVariant = "createVariant",
createInventoryItem = "createInventoryItem",
attachInventoryItem = "attachInventoryItem",
}
const simpleFlow: TransactionStepsDefinition = {
next: {
action: actions.createVariant,
maxRetries: 0,
},
}
const flowWithInventory: TransactionStepsDefinition = {
next: {
action: actions.createVariant,
forwardResponse: true,
maxRetries: 0,
next: {
action: actions.createInventoryItem,
forwardResponse: true,
maxRetries: 0,
next: {
action: actions.attachInventoryItem,
noCompensation: true,
maxRetries: 0,
},
},
},
}
const createSimpleVariantStrategy = new TransactionOrchestrator(
"create-variant",
simpleFlow
)
const createVariantStrategyWithInventory = new TransactionOrchestrator(
"create-variant-with-inventory",
flowWithInventory
)
export default async (req, res) => {
const { id } = req.params
@@ -111,18 +169,140 @@ export default async (req, res) => {
req.body
)
const inventoryService: IInventoryService | undefined =
req.scope.resolve("inventoryService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const productVariantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const productService: ProductService = req.scope.resolve("productService")
const createdId: Record<string, string | null> = {
variant: null,
inventoryItem: null,
}
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await productVariantService
.withTransaction(transactionManager)
.create(id, validated as CreateProductVariantInput)
const inventoryServiceTx =
inventoryService?.withTransaction(transactionManager)
const productVariantInventoryServiceTx =
productVariantInventoryService.withTransaction(transactionManager)
const productVariantServiceTx =
productVariantService.withTransaction(transactionManager)
async function createVariant() {
const variant = await productVariantServiceTx.create(
id,
validated as CreateProductVariantInput
)
createdId.variant = variant.id
return { variant }
}
async function removeVariant() {
if (createdId.variant) {
await productVariantServiceTx.delete(createdId.variant)
}
}
async function createInventoryItem(variant) {
if (!validated.manage_inventory) {
return
}
const inventoryItem = await inventoryServiceTx!.createInventoryItem({
sku: validated.sku,
origin_country: validated.origin_country,
hs_code: validated.hs_code,
mid_code: validated.mid_code,
material: validated.material,
weight: validated.weight,
length: validated.length,
height: validated.height,
width: validated.width,
})
createdId.inventoryItem = inventoryItem.id
return { variant, inventoryItem }
}
async function removeInventoryItem() {
if (createdId.inventoryItem) {
await inventoryServiceTx!.deleteInventoryItem(createdId.inventoryItem)
}
}
async function attachInventoryItem(variant, inventoryItem) {
if (!validated.manage_inventory) {
return
}
await productVariantInventoryServiceTx.attachInventoryItem(
variant.id,
inventoryItem.id,
validated.inventory_quantity
)
}
async function transactionHandler(
actionId: string,
type: TransactionHandlerType,
payload: TransactionPayload
) {
const command = {
[actions.createVariant]: {
[TransactionHandlerType.INVOKE]: async () => {
return await createVariant()
},
[TransactionHandlerType.COMPENSATE]: async () => {
await removeVariant()
},
},
[actions.createInventoryItem]: {
[TransactionHandlerType.INVOKE]: async (data) => {
const { variant } = data._response ?? {}
return await createInventoryItem(variant)
},
[TransactionHandlerType.COMPENSATE]: async () => {
await removeInventoryItem()
},
},
[actions.attachInventoryItem]: {
[TransactionHandlerType.INVOKE]: async (data) => {
const { variant, inventoryItem } = data._response ?? {}
return await attachInventoryItem(variant, inventoryItem)
},
},
}
return command[actionId][type](payload.data)
}
const strategy = inventoryService
? createVariantStrategyWithInventory
: createSimpleVariantStrategy
const transaction = await strategy.beginTransaction(
ulid(),
transactionHandler,
validated
)
await strategy.resume(transaction)
if (transaction.getState() !== TransactionState.DONE) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
transaction.errors.map((err) => err.error?.message).join("\n")
)
}
})
const productService: ProductService = req.scope.resolve("productService")
const product = await productService.retrieve(id, {
select: defaultAdminProductFields,
relations: defaultAdminProductRelations,
@@ -175,6 +355,7 @@ class ProductVariantOptionReq {
* manage_inventory:
* description: Whether Medusa should keep track of the inventory for this Product Variant.
* type: boolean
* default: true
* weight:
* description: The wieght of the Product Variant.
* type: number
@@ -274,7 +455,7 @@ export class AdminPostProductsProductVariantsReq {
@IsBoolean()
@IsOptional()
manage_inventory?: boolean
manage_inventory?: boolean = true
@IsNumber()
@IsOptional()

View File

@@ -1,3 +1,4 @@
import { EntityManager } from "typeorm"
import { FindConfig } from "../../types/common"
import {
@@ -15,6 +16,8 @@ import {
} from "../../types/inventory"
export interface IInventoryService {
withTransaction(transactionManager?: EntityManager): this
listInventoryItems(
selector: FilterableInventoryItemProps,
config?: FindConfig<InventoryItemDTO>

View File

@@ -1,3 +1,4 @@
import { EntityManager } from "typeorm"
import { FindConfig } from "../../types/common"
import {
@@ -8,6 +9,7 @@ import {
} from "../../types/stock-location"
export interface IStockLocationService {
withTransaction(transactionManager?: EntityManager): this
list(
selector: FilterableStockLocationProps,
config?: FindConfig<StockLocationDTO>

View File

@@ -81,7 +81,7 @@ describe("modules loader", () => {
container = buildContainer()
})
it("registers service as false in container when no resolution path is given", async () => {
it("registers service as undefined in container when no resolution path is given", async () => {
const moduleResolutions: Record<string, ModuleResolution> = {
testService: {
resolutionPath: false,
@@ -110,7 +110,7 @@ describe("modules loader", () => {
const testService = container.resolve(
moduleResolutions.testService.definition.key
)
expect(testService).toBe(false)
expect(testService).toBe(undefined)
})
it("registers service ", async () => {

View File

@@ -32,7 +32,7 @@ const registerModule = async (
}
container.register({
[constainerName]: asValue(false),
[constainerName]: asValue(undefined),
})
return {
@@ -42,7 +42,7 @@ const registerModule = async (
if (!resolution.resolutionPath) {
container.register({
[constainerName]: asValue(false),
[constainerName]: asValue(undefined),
})
return

View File

@@ -106,11 +106,13 @@ class ProductVariantInventoryService extends TransactionBaseService {
const hasInventory = await Promise.all(
variantInventory.map(async (inventoryPart) => {
const itemQuantity = inventoryPart.required_quantity * quantity
return await this.inventoryService_.confirmInventory(
inventoryPart.inventory_item_id,
locations,
itemQuantity
)
return await this.inventoryService_
.withTransaction(manager)
.confirmInventory(
inventoryPart.inventory_item_id,
locations,
itemQuantity
)
})
)
@@ -250,9 +252,11 @@ class ProductVariantInventoryService extends TransactionBaseService {
})
// Verify that item exists
await this.inventoryService_.retrieveInventoryItem(inventoryItemId, {
select: ["id"],
})
await this.inventoryService_
.withTransaction(manager)
.retrieveInventoryItem(inventoryItemId, {
select: ["id"],
})
const variantInventoryRepo = manager.getRepository(
ProductVariantInventoryItem
@@ -374,12 +378,14 @@ class ProductVariantInventoryService extends TransactionBaseService {
await Promise.all(
variantInventory.map(async (inventoryPart) => {
const itemQuantity = inventoryPart.required_quantity * quantity
return await this.inventoryService_.createReservationItem({
...toReserve,
location_id: locationId as string,
inventory_item_id: inventoryPart.inventory_item_id,
quantity: itemQuantity,
})
return await this.inventoryService_
.withTransaction(manager)
.createReservationItem({
...toReserve,
location_id: locationId as string,
inventory_item_id: inventoryPart.inventory_item_id,
quantity: itemQuantity,
})
})
)
}
@@ -414,8 +420,11 @@ class ProductVariantInventoryService extends TransactionBaseService {
})
})
}
const [reservations, reservationCount] =
await this.inventoryService_.listReservationItems(
const manager = this.transactionManager_ || this.manager_
const [reservations, reservationCount] = await this.inventoryService_
.withTransaction(manager)
.listReservationItems(
{
line_item_id: lineItemId,
},
@@ -442,11 +451,15 @@ class ProductVariantInventoryService extends TransactionBaseService {
quantity * productVariantInventory.required_quantity
if (reservationQtyUpdate === 0) {
await this.inventoryService_.deleteReservationItem(reservation.id)
await this.inventoryService_
.withTransaction(manager)
.deleteReservationItem(reservation.id)
} else {
await this.inventoryService_.updateReservationItem(reservation.id, {
quantity: reservationQtyUpdate,
})
await this.inventoryService_
.withTransaction(manager)
.updateReservationItem(reservation.id, {
quantity: reservationQtyUpdate,
})
}
}
}
@@ -523,7 +536,10 @@ class ProductVariantInventoryService extends TransactionBaseService {
})
}
await this.inventoryService_.deleteReservationItemsByLineItem(lineItemId)
const manager = this.transactionManager_ || this.manager_
await this.inventoryService_
.withTransaction(manager)
.deleteReservationItemsByLineItem(lineItemId)
}
/**
@@ -554,6 +570,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
})
})
} else {
const manager = this.transactionManager_ || this.manager_
const variantInventory = await this.listByVariant(variantId)
if (variantInventory.length === 0) {
@@ -563,11 +580,13 @@ class ProductVariantInventoryService extends TransactionBaseService {
await Promise.all(
variantInventory.map(async (inventoryPart) => {
const itemQuantity = inventoryPart.required_quantity * quantity
return await this.inventoryService_.adjustInventory(
inventoryPart.inventory_item_id,
locationId,
itemQuantity
)
return await this.inventoryService_
.withTransaction(manager)
.adjustInventory(
inventoryPart.inventory_item_id,
locationId,
itemQuantity
)
})
)
}