fix(medusa): Add inventory decoration for cart endpoints (#5187)

**What**
- decorate item totals for `cart.item.variant` when adding a line-item and retrieving a cart

closes #5181
This commit is contained in:
Philip Korsholm
2023-10-31 09:55:30 +00:00
committed by GitHub
parent ca05436fc1
commit 9ff22110a6
19 changed files with 370 additions and 77 deletions
@@ -1,6 +1,12 @@
import { MedusaError } from "@medusajs/utils"
import {
PricingService,
ProductService,
ProductVariantInventoryService,
SalesChannelService,
} from "../../../../services"
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
import { PricingService, ProductService } from "../../../../services"
import { MedusaError } from "@medusajs/utils"
import { FindParams } from "../../../../types/common"
import { defaultAdminProductRemoteQueryObject } from "./index"
@@ -63,6 +69,12 @@ export default async (req, res) => {
const pricingService: PricingService = req.scope.resolve("pricingService")
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
let rawProduct
if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) {
rawProduct = await getProductWithIsolatedProductModule(
@@ -81,10 +93,29 @@ export default async (req, res) => {
const product = rawProduct
const decoratePromises: Promise<any>[] = []
if (shouldSetPricing) {
await pricingService.setAdminProductPricing([product])
decoratePromises.push(pricingService.setAdminProductPricing([product]))
}
const shouldSetAvailability =
req.retrieveConfig.relations?.includes("variants")
if (shouldSetAvailability) {
const [salesChannelsIds] = await salesChannelService.listAndCount(
{},
{ select: ["id"] }
)
decoratePromises.push(
productVariantInventoryService.setProductAvailability(
[product],
salesChannelsIds.map((salesChannel) => salesChannel.id)
)
)
}
await Promise.all(decoratePromises)
res.json({ product })
}
@@ -1,7 +1,7 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { CartServiceMock } from "../../../../../services/__mocks__/cart"
import { IdMap } from "medusa-test-utils"
import { LineItemServiceMock } from "../../../../../services/__mocks__/line-item"
import { request } from "../../../../../helpers/test-request"
describe("POST /store/carts", () => {
describe("successfully creates a cart", () => {
@@ -1,7 +1,10 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { IsOptional, IsString } from "class-validator"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
@@ -66,6 +69,8 @@ export default async (req, res) => {
const manager: EntityManager = req.scope.resolve("manager")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
await manager.transaction(async (m) => {
const txCartService = cartService.withTransaction(m)
@@ -91,6 +96,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,9 +1,9 @@
import { MedusaContainer } from "@medusajs/modules-sdk"
import {
createCart as createCartWorkflow,
Workflows,
} from "@medusajs/workflows"
import { Type } from "class-transformer"
CartService,
LineItemService,
ProductVariantInventoryService,
RegionService,
} from "../../../../services"
import {
IsArray,
IsInt,
@@ -12,21 +12,23 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { isDefined, MedusaError } from "medusa-core-utils"
import reqIp from "request-ip"
import { EntityManager } from "typeorm"
import { FlagRouter } from "@medusajs/utils"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { LineItem } from "../../../../models"
import { MedusaError, isDefined } from "medusa-core-utils"
import {
CartService,
LineItemService,
RegionService,
} from "../../../../services"
Workflows,
createCart as createCartWorkflow,
} from "@medusajs/workflows"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartCreateProps } from "../../../../types/cart"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import { EntityManager } from "typeorm"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { FlagRouter } from "@medusajs/utils"
import { LineItem } from "../../../../models"
import { MedusaContainer } from "@medusajs/modules-sdk"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { Type } from "class-transformer"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import reqIp from "request-ip"
/**
* @oas [post] /store/carts
@@ -82,6 +84,8 @@ export default async (req, res) => {
const entityManager: EntityManager = req.scope.resolve("manager")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const validated = req.validatedBody as StorePostCartReq
@@ -219,6 +223,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
cart.items.map((i) => i.variant),
cart.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(cart, []) })
}
@@ -2,10 +2,16 @@ import { FlagRouter } from "@medusajs/utils"
import { AwilixContainer } from "awilix"
import { EntityManager } from "typeorm"
import { Cart } from "../../../../../../models"
import { CartService, LineItemService } from "../../../../../../services"
import {
CartService,
LineItemService,
ProductVariantInventoryService,
} from "../../../../../../services"
import { WithRequiredProperty } from "../../../../../../types/common"
import { IdempotencyCallbackResult } from "../../../../../../types/idempotency-key"
import { defaultStoreCartFields, defaultStoreCartRelations } from "../../index"
import SalesChannelFeatureFlag from "../../../../../../loaders/feature-flags/sales-channels"
import { MedusaError } from "medusa-core-utils"
export const CreateLineItemSteps = {
STARTED: "started",
@@ -26,6 +32,9 @@ export async function handleAddOrUpdateLineItem(
const lineItemService: LineItemService = container.resolve("lineItemService")
const featureFlagRouter: FlagRouter = container.resolve("featureFlagRouter")
const productVariantInventoryService: ProductVariantInventoryService =
container.resolve("productVariantInventoryService")
const txCartService = cartService.withTransaction(manager)
let cart = await txCartService.retrieve(cartId, {
@@ -43,17 +52,30 @@ export async function handleAddOrUpdateLineItem(
validateSalesChannels: featureFlagRouter.isFeatureEnabled("sales_channels"),
})
const relations = [
...defaultStoreCartRelations,
"billing_address",
"region.payment_providers",
"payment_sessions",
"customer",
]
const shouldSetAvailability =
relations?.some((rel) => rel.includes("variant")) &&
featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)
cart = await txCartService.retrieveWithTotals(cart.id, {
select: defaultStoreCartFields,
relations: [
...defaultStoreCartRelations,
"billing_address",
"region.payment_providers",
"payment_sessions",
"customer",
],
relations,
})
if (shouldSetAvailability) {
await productVariantInventoryService.setVariantAvailability(
cart.items.map((i) => i.variant),
cart.sales_channel_id!
)
}
if (cart.payment_sessions?.length) {
await txCartService.setPaymentSessions(
cart as WithRequiredProperty<Cart, "total">
@@ -1,5 +1,9 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import IdempotencyKeyService from "../../../../services/idempotency-key"
import { cleanResponseData } from "../../../../utils/clean-response-data"
@@ -55,6 +59,10 @@ export default async (req, res) => {
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
"idempotencyKeyService"
)
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const manager: EntityManager = req.scope.resolve("manager")
const headerKey = req.get("Idempotency-Key") || ""
@@ -98,6 +106,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
cart.items.map((i) => i.variant),
cart.sales_channel_id!
)
return {
response_code: 200,
response_body: { cart },
@@ -1,6 +1,10 @@
import { EntityManager } from "typeorm"
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
@@ -53,6 +57,8 @@ export default async (req, res) => {
const manager: EntityManager = req.scope.resolve("manager")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
await manager.transaction(async (m) => {
// Remove the discount
@@ -73,5 +79,10 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,6 +1,10 @@
import { EntityManager } from "typeorm"
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
@@ -53,6 +57,9 @@ export default async (req, res) => {
const manager: EntityManager = req.scope.resolve("manager")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
await manager.transaction(async (m) => {
const cartServiceTx = cartService.withTransaction(m)
@@ -74,5 +81,10 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,5 +1,9 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
@@ -52,6 +56,9 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await cartService
@@ -64,5 +71,10 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,5 +1,10 @@
import { CartService } from "../../../../services"
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { EntityManager } from "typeorm"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
@@ -50,6 +55,9 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
const manager: EntityManager = req.scope.resolve("manager")
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const cart = await cartService.retrieve(id, {
select: ["id", "customer_id"],
@@ -69,7 +77,27 @@ export default async (req, res) => {
})
}
}
const shouldSetAvailability = req.retrieveConfig.relations?.some((rel) =>
rel.includes("variant")
)
const select = [...req.retrieveConfig.select]
const salesChannelsEnabled = featureFlagRouter.isFeatureEnabled(
SalesChannelFeatureFlag.key
)
if (salesChannelsEnabled) {
select.push("sales_channel_id")
}
const data = await cartService.retrieveWithTotals(id, req.retrieveConfig)
if (shouldSetAvailability) {
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
}
res.json({ cart: cleanResponseData(data, []) })
}
@@ -1,6 +1,9 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { IsString } from "class-validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
@@ -67,6 +70,9 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await cartService
@@ -79,6 +85,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,3 +1,7 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import {
IsArray,
IsEmail,
@@ -7,15 +11,14 @@ import {
} from "class-validator"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { CartService } from "../../../../services"
import { AddressPayload } from "../../../../types/common"
import { EntityManager } from "typeorm"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { IsType } from "../../../../utils/validators/is-type"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { Type } from "class-transformer"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /store/carts/{id}
@@ -79,6 +82,9 @@ export default async (req, res) => {
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
if (req.user?.customer_id) {
validated.customer_id = req.user.customer_id
}
@@ -111,6 +117,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.json({ cart: cleanResponseData(data, []) })
}
@@ -1,7 +1,10 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { IsInt, IsOptional } from "class-validator"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { cleanResponseData } from "../../../../utils/clean-response-data"
@@ -70,6 +73,9 @@ export default async (req, res) => {
const manager: EntityManager = req.scope.resolve("manager")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
await manager.transaction(async (m) => {
// If the quantity is 0 that is effectively deletion
if (validated.quantity === 0) {
@@ -115,6 +121,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,7 +1,11 @@
import { IsObject } from "class-validator"
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { IsObject } from "class-validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
@@ -68,6 +72,8 @@ export default async (req, res) => {
const validated = req.validatedBody
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
@@ -84,6 +90,11 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
data.items.map((i) => i.variant),
data.sales_channel_id!
)
res.status(200).json({ cart: cleanResponseData(data, []) })
}