Feat: TO variant creation (#3097)
This commit is contained in:
committed by
GitHub
parent
a049987215
commit
d50db84a33
@@ -3,6 +3,7 @@ import { IsBoolean, IsNumber, IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { IInventoryService } from "../../../../interfaces"
|
||||
import { FindParams } from "../../../../types/common"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
/**
|
||||
* @oas [post] /inventory-items/{id}
|
||||
@@ -71,11 +72,16 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
const inventoryService: IInventoryService =
|
||||
req.scope.resolve("inventoryService")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
await inventoryService.updateInventoryItem(
|
||||
id,
|
||||
req.validatedBody as AdminPostInventoryItemsInventoryItemReq
|
||||
)
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await inventoryService
|
||||
.withTransaction(transactionManager)
|
||||
.updateInventoryItem(
|
||||
id,
|
||||
req.validatedBody as AdminPostInventoryItemsInventoryItemReq
|
||||
)
|
||||
})
|
||||
|
||||
const inventoryItem = await inventoryService.retrieveInventoryItem(
|
||||
id,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IsOptional, IsNumber } from "class-validator"
|
||||
|
||||
import { IInventoryService } from "../../../../interfaces"
|
||||
import { FindParams } from "../../../../types/common"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
/**
|
||||
* @oas [post] /inventory-items/{id}/location-levels/{location_id}
|
||||
@@ -72,11 +73,16 @@ export default async (req: Request, res: Response) => {
|
||||
|
||||
const inventoryService: IInventoryService =
|
||||
req.scope.resolve("inventoryService")
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
const validatedBody =
|
||||
req.validatedBody as AdminPostInventoryItemsItemLocationLevelsLevelReq
|
||||
|
||||
await inventoryService.updateInventoryLevel(id, location_id, validatedBody)
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await inventoryService
|
||||
.withTransaction(transactionManager)
|
||||
.updateInventoryLevel(id, location_id, validatedBody)
|
||||
})
|
||||
|
||||
const inventoryItem = await inventoryService.retrieveInventoryItem(
|
||||
id,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
|
||||
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
|
||||
import { EventBusServiceMock } from "../../../../../services/__mocks__/event-bus"
|
||||
|
||||
describe("POST /admin/products/:id", () => {
|
||||
describe("successfully updates a product", () => {
|
||||
@@ -9,12 +11,17 @@ describe("POST /admin/products/:id", () => {
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/products/${IdMap.getId("product1")}`,
|
||||
`/admin/products/${IdMap.getId("multipleVariants")}`,
|
||||
{
|
||||
payload: {
|
||||
title: "Product 1",
|
||||
description: "Updated test description",
|
||||
handle: "handle",
|
||||
variants: [
|
||||
{ id: IdMap.getId("variant_1"), title: "Green" },
|
||||
{ title: "Blue" },
|
||||
{ title: "Yellow" },
|
||||
],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
@@ -32,7 +39,7 @@ describe("POST /admin/products/:id", () => {
|
||||
it("calls update", () => {
|
||||
expect(ProductServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ProductServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("product1"),
|
||||
IdMap.getId("multipleVariants"),
|
||||
expect.objectContaining({
|
||||
title: "Product 1",
|
||||
description: "Updated test description",
|
||||
@@ -40,51 +47,35 @@ describe("POST /admin/products/:id", () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully updates variants and create new ones", async () => {
|
||||
expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(2)
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handles failed update operation", () => {
|
||||
it("throws if metadata is to be updated", async () => {
|
||||
try {
|
||||
await request("POST", `/admin/products/${IdMap.getId("product1")}`, {
|
||||
it("throws on wrong variant in update", async () => {
|
||||
const subject = await request(
|
||||
"POST",
|
||||
`/admin/products/${IdMap.getId("variantsWithPrices")}`,
|
||||
{
|
||||
payload: {
|
||||
_id: IdMap.getId("product1"),
|
||||
title: "Product 1",
|
||||
metadata: "Test Description",
|
||||
variants: [{ id: "test_321", title: "Green" }],
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.status).toEqual(400)
|
||||
expect(error.message).toEqual(
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws if variants is to be updated", async () => {
|
||||
try {
|
||||
await request("POST", `/admin/products/${IdMap.getId("product1")}`, {
|
||||
payload: {
|
||||
_id: IdMap.getId("product1"),
|
||||
title: "Product 1",
|
||||
metadata: "Test Description",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.status).toEqual(400)
|
||||
expect(error.message).toEqual(
|
||||
"Use addVariant, reorderVariants, removeVariant to update Product Variants"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(subject.status).toEqual(404)
|
||||
expect(subject.error.text).toEqual(
|
||||
`{"type":"not_found","message":"Variant with id: test_321 is not associated with this product"}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import {
|
||||
PricingService,
|
||||
ProductService,
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
ShippingProfileService,
|
||||
} from "../../../../services"
|
||||
@@ -32,6 +33,14 @@ import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-cha
|
||||
import { ProductStatus } from "../../../../models"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IInventoryService } from "../../../../interfaces"
|
||||
|
||||
import {
|
||||
createVariantTransaction,
|
||||
revertVariantTransaction,
|
||||
} from "./transaction/create-product-variant"
|
||||
import { DistributedTransaction } from "../../../../utils/transaction"
|
||||
import { Logger } from "../../../../types/global"
|
||||
|
||||
/**
|
||||
* @oas [post] /products
|
||||
@@ -98,6 +107,7 @@ import { validator } from "../../../../utils/validator"
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(AdminPostProductsReq, req.body)
|
||||
|
||||
const logger: Logger = req.scope.resolve("logger")
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
@@ -106,6 +116,10 @@ export default async (req, res) => {
|
||||
const shippingProfileService: ShippingProfileService = req.scope.resolve(
|
||||
"shippingProfileService"
|
||||
)
|
||||
const productVariantInventoryService: ProductVariantInventoryService =
|
||||
req.scope.resolve("productVariantInventoryService")
|
||||
const inventoryService: IInventoryService | undefined =
|
||||
req.scope.resolve("inventoryService")
|
||||
|
||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
@@ -134,8 +148,8 @@ export default async (req, res) => {
|
||||
.create({ ...validated, profile_id: shippingProfile.id })
|
||||
|
||||
if (variants) {
|
||||
for (const [i, variant] of variants.entries()) {
|
||||
variant["variant_rank"] = i
|
||||
for (const [index, variant] of variants.entries()) {
|
||||
variant["variant_rank"] = index
|
||||
}
|
||||
|
||||
const optionIds =
|
||||
@@ -143,22 +157,48 @@ export default async (req, res) => {
|
||||
(o) => newProduct.options.find((newO) => newO.title === o.title)?.id
|
||||
) || []
|
||||
|
||||
await Promise.all(
|
||||
variants.map(async (v) => {
|
||||
const variant = {
|
||||
...v,
|
||||
options:
|
||||
v?.options?.map((o, index) => ({
|
||||
...o,
|
||||
option_id: optionIds[index],
|
||||
})) || [],
|
||||
}
|
||||
const allVariantTransactions: DistributedTransaction[] = []
|
||||
const transactionDependencies = {
|
||||
manager,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
}
|
||||
|
||||
await productVariantService
|
||||
.withTransaction(manager)
|
||||
.create(newProduct.id, variant as CreateProductVariantInput)
|
||||
})
|
||||
)
|
||||
try {
|
||||
await Promise.all(
|
||||
variants.map(async (variant) => {
|
||||
const options =
|
||||
variant?.options?.map((option, index) => ({
|
||||
...option,
|
||||
option_id: optionIds[index],
|
||||
})) || []
|
||||
|
||||
const input = {
|
||||
...variant,
|
||||
options,
|
||||
}
|
||||
|
||||
const varTransation = await createVariantTransaction(
|
||||
transactionDependencies,
|
||||
newProduct.id,
|
||||
input as CreateProductVariantInput
|
||||
)
|
||||
allVariantTransactions.push(varTransation)
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
await revertVariantTransaction(
|
||||
transactionDependencies,
|
||||
transaction
|
||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||
})
|
||||
)
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return newProduct
|
||||
|
||||
@@ -22,18 +22,10 @@ import {
|
||||
} 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"
|
||||
|
||||
import { createVariantTransaction } from "./transaction/create-product-variant"
|
||||
|
||||
/**
|
||||
* @oas [post] /products/{id}/variants
|
||||
* operationId: "PostProductsProductVariants"
|
||||
@@ -122,47 +114,6 @@ import { EntityManager } from "typeorm"
|
||||
* $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
|
||||
|
||||
@@ -179,129 +130,19 @@ export default async (req, res) => {
|
||||
"productVariantService"
|
||||
)
|
||||
|
||||
const createdId: Record<string, string | null> = {
|
||||
variant: null,
|
||||
inventoryItem: null,
|
||||
}
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
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 createVariantTransaction(
|
||||
{
|
||||
manager: transactionManager,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
},
|
||||
id,
|
||||
validated as CreateProductVariantInput
|
||||
)
|
||||
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")
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import {
|
||||
DistributedTransaction,
|
||||
TransactionHandlerType,
|
||||
TransactionOrchestrator,
|
||||
TransactionPayload,
|
||||
TransactionState,
|
||||
TransactionStepsDefinition,
|
||||
} from "../../../../../utils/transaction"
|
||||
import { ulid } from "ulid"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IInventoryService } from "../../../../../interfaces"
|
||||
import {
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../../services"
|
||||
import { CreateProductVariantInput } from "../../../../../types/product-variant"
|
||||
import { InventoryItemDTO } from "../../../../../types/inventory"
|
||||
import { ProductVariant } from "../../../../../models"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
enum actions {
|
||||
createVariant = "createVariant",
|
||||
createInventoryItem = "createInventoryItem",
|
||||
attachInventoryItem = "attachInventoryItem",
|
||||
}
|
||||
|
||||
const simpleFlow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: actions.createVariant,
|
||||
},
|
||||
}
|
||||
|
||||
const flowWithInventory: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: actions.createVariant,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: actions.createInventoryItem,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: actions.attachInventoryItem,
|
||||
noCompensation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const createSimpleVariantStrategy = new TransactionOrchestrator(
|
||||
"create-variant",
|
||||
simpleFlow
|
||||
)
|
||||
|
||||
const createVariantStrategyWithInventory = new TransactionOrchestrator(
|
||||
"create-variant-with-inventory",
|
||||
flowWithInventory
|
||||
)
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
productVariantService: ProductVariantService
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
inventoryService?: IInventoryService
|
||||
}
|
||||
|
||||
export const createVariantTransaction = async (
|
||||
dependencies: InjectedDependencies,
|
||||
productId: string,
|
||||
input: CreateProductVariantInput
|
||||
): Promise<DistributedTransaction> => {
|
||||
const {
|
||||
manager,
|
||||
productVariantService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
} = dependencies
|
||||
|
||||
const inventoryServiceTx = inventoryService?.withTransaction(manager)
|
||||
|
||||
const productVariantInventoryServiceTx =
|
||||
productVariantInventoryService.withTransaction(manager)
|
||||
|
||||
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
||||
|
||||
async function createVariant(variantInput: CreateProductVariantInput) {
|
||||
const variant = await productVariantServiceTx.create(
|
||||
productId,
|
||||
variantInput
|
||||
)
|
||||
|
||||
return { variant }
|
||||
}
|
||||
|
||||
async function removeVariant(variant: ProductVariant) {
|
||||
if (variant) {
|
||||
await productVariantServiceTx.delete(variant.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function createInventoryItem(variant: ProductVariant) {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
const inventoryItem = await inventoryServiceTx!.createInventoryItem({
|
||||
sku: variant.sku,
|
||||
origin_country: variant.origin_country,
|
||||
hs_code: variant.hs_code,
|
||||
mid_code: variant.mid_code,
|
||||
material: variant.material,
|
||||
weight: variant.weight,
|
||||
length: variant.length,
|
||||
height: variant.height,
|
||||
width: variant.width,
|
||||
})
|
||||
|
||||
return { inventoryItem }
|
||||
}
|
||||
|
||||
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
|
||||
if (inventoryItem) {
|
||||
await inventoryServiceTx!.deleteInventoryItem(inventoryItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function attachInventoryItem(
|
||||
variant: ProductVariant,
|
||||
inventoryItem: InventoryItemDTO
|
||||
) {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
await productVariantInventoryServiceTx.attachInventoryItem(
|
||||
variant.id,
|
||||
inventoryItem.id
|
||||
)
|
||||
}
|
||||
|
||||
async function transactionHandler(
|
||||
actionId: string,
|
||||
type: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
[actions.createVariant]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput
|
||||
) => {
|
||||
return await createVariant(data)
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
{ invoke }
|
||||
) => {
|
||||
await removeVariant(invoke[actions.createVariant])
|
||||
},
|
||||
},
|
||||
[actions.createInventoryItem]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
{ invoke }
|
||||
) => {
|
||||
const { [actions.createVariant]: variant } = invoke
|
||||
|
||||
return await createInventoryItem(variant)
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
{ invoke }
|
||||
) => {
|
||||
await removeInventoryItem(invoke[actions.createInventoryItem])
|
||||
},
|
||||
},
|
||||
[actions.attachInventoryItem]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
{ invoke }
|
||||
) => {
|
||||
const {
|
||||
[actions.createVariant]: variant,
|
||||
[actions.createInventoryItem]: inventoryItem,
|
||||
} = invoke
|
||||
|
||||
return await attachInventoryItem(variant, inventoryItem)
|
||||
},
|
||||
},
|
||||
}
|
||||
return command[actionId][type](payload.data, payload.context)
|
||||
}
|
||||
|
||||
const strategy = inventoryService
|
||||
? createVariantStrategyWithInventory
|
||||
: createSimpleVariantStrategy
|
||||
|
||||
const transaction = await strategy.beginTransaction(
|
||||
ulid(),
|
||||
transactionHandler,
|
||||
input
|
||||
)
|
||||
await strategy.resume(transaction)
|
||||
|
||||
if (transaction.getState() !== TransactionState.DONE) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
transaction
|
||||
.getErrors()
|
||||
.map((err) => err.error?.message)
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
return transaction
|
||||
}
|
||||
|
||||
export const revertVariantTransaction = async (
|
||||
dependencies: InjectedDependencies,
|
||||
transaction: DistributedTransaction
|
||||
) => {
|
||||
const { inventoryService } = dependencies
|
||||
const strategy = inventoryService
|
||||
? createVariantStrategyWithInventory
|
||||
: createSimpleVariantStrategy
|
||||
|
||||
await strategy.cancelTransaction(transaction)
|
||||
}
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import { PricingService, ProductService } from "../../../../services"
|
||||
import {
|
||||
PricingService,
|
||||
ProductService,
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../services"
|
||||
import {
|
||||
ProductSalesChannelReq,
|
||||
ProductTagReq,
|
||||
@@ -23,10 +28,21 @@ import {
|
||||
import { Type } from "class-transformer"
|
||||
import { EntityManager } from "typeorm"
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||
import { ProductStatus } from "../../../../models"
|
||||
import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant"
|
||||
import { ProductStatus, ProductVariant } from "../../../../models"
|
||||
import {
|
||||
CreateProductVariantInput,
|
||||
ProductVariantPricesUpdateReq,
|
||||
} from "../../../../types/product-variant"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { DistributedTransaction } from "../../../../utils/transaction"
|
||||
import {
|
||||
createVariantTransaction,
|
||||
revertVariantTransaction,
|
||||
} from "./transaction/create-product-variant"
|
||||
import { IInventoryService } from "../../../../interfaces"
|
||||
import { Logger } from "../../../../types/global"
|
||||
|
||||
/**
|
||||
* @oas [post] /products/{id}
|
||||
@@ -96,14 +112,106 @@ export default async (req, res) => {
|
||||
|
||||
const validated = await validator(AdminPostProductsProductReq, req.body)
|
||||
|
||||
const logger: Logger = req.scope.resolve("logger")
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
const productVariantInventoryService: ProductVariantInventoryService =
|
||||
req.scope.resolve("productVariantInventoryService")
|
||||
const inventoryService: IInventoryService | undefined =
|
||||
req.scope.resolve("inventoryService")
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
const { variants } = validated
|
||||
delete validated.variants
|
||||
|
||||
await productService
|
||||
.withTransaction(transactionManager)
|
||||
.update(id, validated)
|
||||
|
||||
if (!variants) {
|
||||
return
|
||||
}
|
||||
|
||||
const product = await productService
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(id, {
|
||||
relations: ["variants"],
|
||||
})
|
||||
|
||||
// Iterate product variants and update their properties accordingly
|
||||
for (const variant of product.variants) {
|
||||
const exists = variants.find((v) => v.id && variant.id === v.id)
|
||||
if (!exists) {
|
||||
await productVariantService
|
||||
.withTransaction(transactionManager)
|
||||
.delete(variant.id)
|
||||
}
|
||||
}
|
||||
|
||||
const allVariantTransactions: DistributedTransaction[] = []
|
||||
const transactionDependencies = {
|
||||
manager: transactionManager,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
}
|
||||
|
||||
for (const [index, newVariant] of variants.entries()) {
|
||||
const variantRank = index
|
||||
|
||||
if (newVariant.id) {
|
||||
const variant = product.variants.find((v) => v.id === newVariant.id)
|
||||
|
||||
if (!variant) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variant with id: ${newVariant.id} is not associated with this product`
|
||||
)
|
||||
}
|
||||
|
||||
await productVariantService
|
||||
.withTransaction(transactionManager)
|
||||
.update(variant, {
|
||||
...newVariant,
|
||||
variant_rank: variantRank,
|
||||
product_id: variant.product_id,
|
||||
})
|
||||
} else {
|
||||
// If the provided variant does not have an id, we assume that it
|
||||
// should be created
|
||||
|
||||
try {
|
||||
const input = {
|
||||
...newVariant,
|
||||
variant_rank: variantRank,
|
||||
options: newVariant.options || [],
|
||||
prices: newVariant.prices || [],
|
||||
}
|
||||
|
||||
const varTransation = await createVariantTransaction(
|
||||
transactionDependencies,
|
||||
product.id,
|
||||
input as CreateProductVariantInput
|
||||
)
|
||||
allVariantTransactions.push(varTransation)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
await revertVariantTransaction(
|
||||
transactionDependencies,
|
||||
transaction
|
||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||
})
|
||||
)
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const rawProduct = await productService.retrieve(id, {
|
||||
|
||||
Reference in New Issue
Block a user