Skip to content

Commit

Permalink
fix(medusa): Add inventory decoration for cart endpoints (#5187)
Browse files Browse the repository at this point in the history
**What**
- decorate item totals for `cart.item.variant` when adding a line-item and retrieving a cart

closes #5181
  • Loading branch information
pKorsholm authored Oct 31, 2023
1 parent ca05436 commit 9ff2211
Show file tree
Hide file tree
Showing 19 changed files with 370 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-maps-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

fix(medusa): decorate inventory for variants returned through carts
48 changes: 48 additions & 0 deletions integration-tests/plugins/__tests__/inventory/cart/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,54 @@ describe("/store/carts", () => {
expect(count).toEqual(0)
})

it("should decorate line item variant inventory_quantity when creating a line-item", async () => {
const api = useApi()

const cartId = "test-cart"

// Add standard line item to cart
const addCart = await api
.post(
`/store/carts/${cartId}/line-items`,
{
variant_id: variantId,
quantity: 3,
},
{ withCredentials: true }
)
.catch((e) => e)

expect(addCart.status).toEqual(200)
expect(addCart.data.cart.items[0].variant.inventory_quantity).toEqual(5)
})

it("should decorate line item variant inventory_quantity when getting cart", async () => {
const api = useApi()

const cartId = "test-cart"

// Add standard line item to cart
await api
.post(
`/store/carts/${cartId}/line-items`,
{
variant_id: variantId,
quantity: 3,
},
{ withCredentials: true }
)
.catch((e) => e)

const cartResponse = await api
.get(`/store/carts/${cartId}`, { withCredentials: true })
.catch((e) => e)

expect(cartResponse.status).toEqual(200)
expect(
cartResponse.data.cart.items[0].variant.inventory_quantity
).toEqual(5)
})

it("fails to add a item on the cart if the inventory isn't enough", async () => {
const api = useApi()

Expand Down
100 changes: 73 additions & 27 deletions integration-tests/plugins/__tests__/inventory/products/get-product.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ const adminSeeder = require("../../../../helpers/admin-seeder")

jest.setTimeout(30000)

const { simpleProductFactory } = require("../../../../factories")
const {
simpleProductFactory,
simpleSalesChannelFactory,
} = require("../../../../factories")

const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }

Expand Down Expand Up @@ -53,7 +56,17 @@ describe("Get products", () => {
"productVariantInventoryService"
)
const inventoryService = appContainer.resolve("inventoryService")
const locationService = appContainer.resolve("stockLocationService")
const salesChannelService = appContainer.resolve("salesChannelService")
const salesChannelLocationService = appContainer.resolve(
"salesChannelLocationService"
)

const salesChannel = await simpleSalesChannelFactory(dbConnection, {
is_default: true,
})

const location = await locationService.create({ name: "test-location" })
await simpleProductFactory(
dbConnection,
{
Expand All @@ -63,7 +76,11 @@ describe("Get products", () => {
},
100
)

await salesChannelService.addProducts(salesChannel.id, [productId])
await salesChannelLocationService.associateLocation(
salesChannel.id,
location.id
)
invItem = await inventoryService.createInventoryItem({
sku: "test-sku",
})
Expand All @@ -72,33 +89,62 @@ describe("Get products", () => {
variantId,
invItem.id
)
})

it("Expands inventory items when getting product with expand parameters", async () => {
const api = useApi()
await inventoryService.createInventoryLevel({
inventory_item_id: invItem.id,
location_id: location.id,
stocked_quantity: 100,
})
})

const res = await api.get(
`/store/products/${productId}?expand=variants,variants.inventory_items`,
adminHeaders
)
describe("/store/products/:id", () => {
it("Expands inventory items when getting product with expand parameters", async () => {
const api = useApi()

const res = await api.get(
`/store/products/${productId}?expand=variants,variants.inventory_items`
)

expect(res.status).toEqual(200)
expect(res.data.product).toEqual(
expect.objectContaining({
id: productId,
variants: [
expect.objectContaining({
id: variantId,
inventory_items: [
expect.objectContaining({
inventory_item_id: invItem.id,
variant_id: variantId,
}),
],
}),
],
}),
expect.objectContaining({})
)
})
})

expect(res.status).toEqual(200)
expect(res.data.product).toEqual(
expect.objectContaining({
id: productId,
variants: [
expect.objectContaining({
id: variantId,
inventory_items: [
expect.objectContaining({
inventory_item_id: invItem.id,
variant_id: variantId,
}),
],
}),
],
}),
expect.objectContaining({})
)
describe("/admin/products/:id", () => {
it("should get inventory quantity for products fetched through the admin api", async () => {
const api = useApi()

const res = await api.get(`/admin/products/${productId}`, adminHeaders)

expect(res.status).toEqual(200)
expect(res.data.product).toEqual(
expect.objectContaining({
id: productId,
variants: [
expect.objectContaining({
id: variantId,
inventory_quantity: 100,
}),
],
}),
expect.objectContaining({})
)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import path from "path"
import adminSeeder from "../../../../helpers/admin-seeder"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
import { createVariantPriceSet } from "../../../helpers/create-variant-price-set"
import { AxiosInstance } from "axios"

jest.setTimeout(50000)

Expand Down Expand Up @@ -80,7 +81,7 @@ describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", ()
})

it("should create product variant price sets and prices", async () => {
const api = useApi()
const api = useApi()! as AxiosInstance
const data = {
title: "test variant update",
prices: [
Expand Down Expand Up @@ -144,7 +145,7 @@ describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", ()

const moneyAmountToUpdate = priceSet.money_amounts?.[0]

const api = useApi()
const api = useApi()! as AxiosInstance
const data = {
title: "test variant update",
prices: [
Expand Down Expand Up @@ -202,7 +203,7 @@ describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", ()
prices: [],
})

const api = useApi()
const api = useApi()! as AxiosInstance
const data = {
title: "test variant update",
prices: [
Expand Down
37 changes: 34 additions & 3 deletions packages/medusa/src/api/routes/admin/products/get-product.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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(
Expand All @@ -81,9 +93,28 @@ 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 })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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)
Expand All @@ -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, []) })
}

Expand Down
41 changes: 25 additions & 16 deletions packages/medusa/src/api/routes/store/carts/create-cart.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,21 +12,23 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { isDefined, MedusaError } from "medusa-core-utils"
import reqIp from "request-ip"
import { MedusaError, isDefined } from "medusa-core-utils"
import {
Workflows,
createCart as createCartWorkflow,
} from "@medusajs/workflows"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."

import { CartCreateProps } from "../../../../types/cart"
import { EntityManager } from "typeorm"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { FlagRouter } from "@medusajs/utils"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { LineItem } from "../../../../models"
import {
CartService,
LineItemService,
RegionService,
} from "../../../../services"
import { CartCreateProps } from "../../../../types/cart"
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 { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import reqIp from "request-ip"

/**
* @oas [post] /store/carts
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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, []) })
}

Expand Down
Loading

0 comments on commit 9ff2211

Please sign in to comment.