diff --git a/.changeset/plenty-cats-thank.md b/.changeset/plenty-cats-thank.md new file mode 100644 index 0000000000..fc1a4d489d --- /dev/null +++ b/.changeset/plenty-cats-thank.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): add purchasable property diff --git a/docs/api/admin/components/schemas/DecoratedProduct.yaml b/docs/api/admin/components/schemas/DecoratedProduct.yaml new file mode 100644 index 0000000000..6bf5e38002 --- /dev/null +++ b/docs/api/admin/components/schemas/DecoratedProduct.yaml @@ -0,0 +1,10 @@ +title: Product with decorated variants +type: object +allOf: + - $ref: ./Product.yaml + - type: object + properties: + variants: + type: array + items: + $ref: ./DecoratedVariant.yaml diff --git a/docs/api/admin/components/schemas/DecoratedVariant.yaml b/docs/api/admin/components/schemas/DecoratedVariant.yaml new file mode 100644 index 0000000000..64161130ef --- /dev/null +++ b/docs/api/admin/components/schemas/DecoratedVariant.yaml @@ -0,0 +1,9 @@ +title: Decorated Product Variant +type: object +allOf: + - $ref: ./PricedVariant.yaml + - type: object + properties: + purchasable: + type: boolean + description: Boolean indicating if variant is purchasable. diff --git a/docs/api/store/components/schemas/DecoratedProduct.yaml b/docs/api/store/components/schemas/DecoratedProduct.yaml new file mode 100644 index 0000000000..6bf5e38002 --- /dev/null +++ b/docs/api/store/components/schemas/DecoratedProduct.yaml @@ -0,0 +1,10 @@ +title: Product with decorated variants +type: object +allOf: + - $ref: ./Product.yaml + - type: object + properties: + variants: + type: array + items: + $ref: ./DecoratedVariant.yaml diff --git a/docs/api/store/components/schemas/DecoratedVariant.yaml b/docs/api/store/components/schemas/DecoratedVariant.yaml new file mode 100644 index 0000000000..64161130ef --- /dev/null +++ b/docs/api/store/components/schemas/DecoratedVariant.yaml @@ -0,0 +1,9 @@ +title: Decorated Product Variant +type: object +allOf: + - $ref: ./PricedVariant.yaml + - type: object + properties: + purchasable: + type: boolean + description: Boolean indicating if variant is purchasable. diff --git a/integration-tests/plugins/__tests__/inventory/products/list-products.js b/integration-tests/plugins/__tests__/inventory/products/list-products.js index 9857212122..f605b67979 100644 --- a/integration-tests/plugins/__tests__/inventory/products/list-products.js +++ b/integration-tests/plugins/__tests__/inventory/products/list-products.js @@ -50,6 +50,7 @@ describe("Create Variant", () => { describe("list-products", () => { const productId = "test-product" const variantId = "test-variant" + let sc2 beforeEach(async () => { await adminSeeder(dbConnection) @@ -66,13 +67,14 @@ describe("Create Variant", () => { dbConnection, { id: productId, + status: "published", variants: [{ id: variantId }], }, 100 ) const sc1 = await simpleSalesChannelFactory(dbConnection, {}) - const sc2 = await simpleSalesChannelFactory(dbConnection, {}) + sc2 = await simpleSalesChannelFactory(dbConnection, {}) const sc3 = await simpleSalesChannelFactory(dbConnection, {}) const sl1 = await stockLocationService.create({ name: "sl1" }) @@ -120,5 +122,203 @@ describe("Create Variant", () => { }), ]) }) + + describe("/store/products", () => { + beforeEach(async () => { + const inventoryService = appContainer.resolve("inventoryService") + const productVariantInventoryService = appContainer.resolve( + "productVariantInventoryService" + ) + + await simpleProductFactory( + dbConnection, + { + id: `${productId}-1`, + status: "published", + variants: [ + { + id: `${variantId}-1`, + manage_inventory: false, + }, + ], + }, + 101 + ) + await simpleProductFactory( + dbConnection, + { + id: `${productId}-2`, + status: "published", + variants: [{ id: `${variantId}-2`, manage_inventory: true }], + }, + 102 + ) + await simpleProductFactory( + dbConnection, + { + id: `${productId}-3`, + status: "published", + variants: [ + { + id: `${variantId}-3`, + manage_inventory: true, + allow_backorder: true, + }, + ], + }, + 103 + ) + const invItem = await inventoryService.createInventoryItem({ + sku: "test-sku-1", + }) + await productVariantInventoryService.attachInventoryItem( + `${variantId}-3`, + invItem.id + ) + await simpleProductFactory( + dbConnection, + { + id: `${productId}-4`, + status: "published", + variants: [ + { + id: `${variantId}-4`, + manage_inventory: true, + allow_backorder: false, + }, + ], + }, + 104 + ) + const invItem1 = await inventoryService.createInventoryItem({ + sku: "test-sku-2", + }) + await productVariantInventoryService.attachInventoryItem( + `${variantId}-4`, + invItem1.id + ) + }) + + it("lists location availability correctly for store", async () => { + const api = useApi() + + const res = await api.get(`/store/products`) + + expect(res.status).toEqual(200) + expect(res.data.products).toEqual([ + expect.objectContaining({ + id: `${productId}-4`, + variants: [ + expect.objectContaining({ + purchasable: false, + }), + ], + }), + expect.objectContaining({ + id: `${productId}-3`, + variants: [ + expect.objectContaining({ + purchasable: false, + }), + ], + }), + expect.objectContaining({ + id: `${productId}-2`, + variants: [ + expect.objectContaining({ + purchasable: true, + }), + ], + }), + expect.objectContaining({ + id: `${productId}-1`, + variants: [ + expect.objectContaining({ + inventory_quantity: 10, + purchasable: true, + }), + ], + }), + expect.objectContaining({ + id: productId, + variants: [ + expect.objectContaining({ + purchasable: false, + }), + ], + }), + ]) + }) + + it("lists location availability correctly for store with sales channel id", async () => { + const api = useApi() + + const productService = appContainer.resolve("productService") + + const ids = [ + `${productId}`, + `${productId}-1`, + `${productId}-2`, + `${productId}-3`, + `${productId}-4`, + ] + + for (const id of ids) { + await productService.update(id, { + sales_channels: [{ id: sc2.id }], + }) + } + + const res = await api.get( + `/store/products?sales_channel_id[]=${sc2.id}` + ) + + expect(res.status).toEqual(200) + expect(res.data.products).toEqual([ + expect.objectContaining({ + id: `${productId}-4`, + variants: [ + expect.objectContaining({ + purchasable: false, + }), + ], + }), + expect.objectContaining({ + id: `${productId}-3`, + variants: [ + expect.objectContaining({ + purchasable: true, + }), + ], + }), + expect.objectContaining({ + id: `${productId}-2`, + variants: [ + expect.objectContaining({ + purchasable: true, + }), + ], + }), + expect.objectContaining({ + id: `${productId}-1`, + variants: [ + expect.objectContaining({ + inventory_quantity: 10, + purchasable: true, + }), + ], + }), + expect.objectContaining({ + id: productId, + variants: [ + expect.objectContaining({ + purchasable: true, + inventory_quantity: 4, + }), + ], + }), + ]) + }) + }) }) }) diff --git a/integration-tests/plugins/factories/simple-product-factory.ts b/integration-tests/plugins/factories/simple-product-factory.ts index 23288ad6bc..c321037bc5 100644 --- a/integration-tests/plugins/factories/simple-product-factory.ts +++ b/integration-tests/plugins/factories/simple-product-factory.ts @@ -1,14 +1,15 @@ +import { Product, ProductOption, ProductStatus, ProductType, ShippingProfile, ShippingProfileType, } from "@medusajs/medusa" +import { ProductVariantFactoryData, simpleProductVariantFactory, } from "./simple-product-variant-factory" + import { Connection } from "typeorm" import faker from "faker" -import { Product, ProductOption, ProductType, ShippingProfile, ShippingProfileType, } from "@medusajs/medusa" - -import { ProductVariantFactoryData, simpleProductVariantFactory, } from "./simple-product-variant-factory" export type ProductFactoryData = { id?: string is_giftcard?: boolean title?: string type?: string + status?: ProductStatus options?: { id: string; title: string }[] variants?: ProductVariantFactoryData[] } @@ -45,6 +46,7 @@ export const simpleProductFactory = async ( const toSave = manager.create(Product, { id: prodId, type_id: typeId, + status: data.status || ProductStatus.DRAFT, title: data.title || faker.commerce.productName(), is_giftcard: data.is_giftcard || false, discountable: !data.is_giftcard, diff --git a/integration-tests/plugins/factories/simple-product-variant-factory.ts b/integration-tests/plugins/factories/simple-product-variant-factory.ts index 8d6f6f9cc9..81913e44ad 100644 --- a/integration-tests/plugins/factories/simple-product-variant-factory.ts +++ b/integration-tests/plugins/factories/simple-product-variant-factory.ts @@ -1,17 +1,20 @@ -import { Connection } from "typeorm" -import faker from "faker" import { + MoneyAmount, ProductOptionValue, ProductVariant, - MoneyAmount, } from "@medusajs/medusa" +import { Connection } from "typeorm" +import faker from "faker" + export type ProductVariantFactoryData = { product_id: string id?: string is_giftcard?: boolean inventory_quantity?: number title?: string + allow_backorder?: boolean + manage_inventory?: boolean options?: { option_id: string; value: string }[] prices?: { currency: string; amount: number }[] } @@ -31,6 +34,8 @@ export const simpleProductVariantFactory = async ( const toSave = manager.create(ProductVariant, { id, product_id: data.product_id, + allow_backorder: data.allow_backorder || false, + manage_inventory: typeof data.manage_inventory !== 'undefined' ? data.manage_inventory : true, inventory_quantity: typeof data.inventory_quantity !== "undefined" ? data.inventory_quantity diff --git a/packages/generated/client-types/src/lib/models/AdminProductsListRes.ts b/packages/generated/client-types/src/lib/models/AdminProductsListRes.ts index e15082d115..25602e690b 100644 --- a/packages/generated/client-types/src/lib/models/AdminProductsListRes.ts +++ b/packages/generated/client-types/src/lib/models/AdminProductsListRes.ts @@ -14,7 +14,9 @@ export interface AdminProductsListRes { "collection" | "images" | "options" | "tags" | "type" | "variants" >, { - variants: Array> + variants: Array< + SetRelation + > } > > diff --git a/packages/generated/client-types/src/lib/models/AdminVariantsListRes.ts b/packages/generated/client-types/src/lib/models/AdminVariantsListRes.ts index d6708fc3c5..791be5604f 100644 --- a/packages/generated/client-types/src/lib/models/AdminVariantsListRes.ts +++ b/packages/generated/client-types/src/lib/models/AdminVariantsListRes.ts @@ -6,7 +6,9 @@ import { SetRelation, Merge } from "../core/ModelUtils" import type { PricedVariant } from "./PricedVariant" export interface AdminVariantsListRes { - variants: Array> + variants: Array< + SetRelation + > /** * The total number of items available */ diff --git a/packages/generated/client-types/src/lib/models/ProductVariant.ts b/packages/generated/client-types/src/lib/models/ProductVariant.ts index e96c086de8..ce521be311 100644 --- a/packages/generated/client-types/src/lib/models/ProductVariant.ts +++ b/packages/generated/client-types/src/lib/models/ProductVariant.ts @@ -120,4 +120,15 @@ export interface ProductVariant { * An optional key-value map with additional details */ metadata: Record | null + /** + * Only used with the inventory modules. + * A boolean value indicating whether the Product Variant is purchasable. + * A variant is purchasable if: + * - inventory is not managed + * - it has no inventory items + * - it is in stock + * - it is backorderable. + * + */ + purchasable?: boolean } diff --git a/packages/generated/client-types/src/lib/models/StoreProductsListRes.ts b/packages/generated/client-types/src/lib/models/StoreProductsListRes.ts index f850e16b1e..d9c33d58ad 100644 --- a/packages/generated/client-types/src/lib/models/StoreProductsListRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreProductsListRes.ts @@ -16,7 +16,9 @@ export interface StoreProductsListRes { >, { options: Array> - variants: Array> + variants: Array< + SetRelation + > } > > diff --git a/packages/generated/client-types/src/lib/models/StoreProductsRes.ts b/packages/generated/client-types/src/lib/models/StoreProductsRes.ts index 8be3fc3e6d..2b3db7ecd0 100644 --- a/packages/generated/client-types/src/lib/models/StoreProductsRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreProductsRes.ts @@ -15,7 +15,9 @@ export interface StoreProductsRes { >, { options: Array> - variants: Array> + variants: Array< + SetRelation + > } > } diff --git a/packages/generated/client-types/src/lib/models/StoreVariantsListRes.ts b/packages/generated/client-types/src/lib/models/StoreVariantsListRes.ts index e73739f319..474534369e 100644 --- a/packages/generated/client-types/src/lib/models/StoreVariantsListRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreVariantsListRes.ts @@ -6,5 +6,7 @@ import { SetRelation, Merge } from "../core/ModelUtils" import type { PricedVariant } from "./PricedVariant" export interface StoreVariantsListRes { - variants: Array> + variants: Array< + SetRelation + > } diff --git a/packages/generated/client-types/src/lib/models/StoreVariantsRes.ts b/packages/generated/client-types/src/lib/models/StoreVariantsRes.ts index f9c33d6171..81e96326d6 100644 --- a/packages/generated/client-types/src/lib/models/StoreVariantsRes.ts +++ b/packages/generated/client-types/src/lib/models/StoreVariantsRes.ts @@ -6,5 +6,8 @@ import { SetRelation, Merge } from "../core/ModelUtils" import type { PricedVariant } from "./PricedVariant" export interface StoreVariantsRes { - variant: SetRelation + variant: SetRelation< + PricedVariant, + "prices" | "options" | "product" | "purchasable" + > } diff --git a/packages/medusa/src/api/routes/admin/draft-orders/register-payment.ts b/packages/medusa/src/api/routes/admin/draft-orders/register-payment.ts index 3c70863820..7d69dab67f 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/register-payment.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/register-payment.ts @@ -114,13 +114,6 @@ export default async (req, res) => { select: defaultOrderFields, }) - // TODO: Re-enable when we have a way to handle inventory for draft orders on creation - if (!inventoryService) { - await reserveQuantityForDraftOrder(order, { - productVariantInventoryService, - }) - } - return order }) diff --git a/packages/medusa/src/api/routes/admin/products/index.ts b/packages/medusa/src/api/routes/admin/products/index.ts index 75385045b0..08ee8f539b 100644 --- a/packages/medusa/src/api/routes/admin/products/index.ts +++ b/packages/medusa/src/api/routes/admin/products/index.ts @@ -1,12 +1,14 @@ -import { Router } from "express" import "reflect-metadata" -import { Product, ProductTag, ProductType, ProductVariant } from "../../../.." + import { FindParams, PaginatedResponse } from "../../../../types/common" -import { PricedProduct } from "../../../../types/pricing" -import { FlagRouter } from "../../../../utils/flag-router" +import { Product, ProductTag, ProductType, ProductVariant } from "../../../.." import middlewares, { transformQuery } from "../../../middlewares" -import { validateSalesChannelsExist } from "../../../middlewares/validators/sales-channel-existence" + import { AdminGetProductsParams } from "./list-products" +import { FlagRouter } from "../../../../utils/flag-router" +import { Router } from "express" +import { validateSalesChannelsExist } from "../../../middlewares/validators/sales-channel-existence" +import { PricedProduct } from "../../../../types/pricing" const route = Router() @@ -258,6 +260,8 @@ export type AdminProductsDeleteRes = { * - variants * - variants.options * - variants.prices + * totals: + * - variants.purchasable * required: * - products * - count diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index a85695e365..abfdea4d0c 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -7,10 +7,10 @@ import { } from "../../../../services" import { FilterableProductProps } from "../../../../types/product" +import { IInventoryService } from "@medusajs/types" import { PricedProduct } from "../../../../types/pricing" import { Product } from "../../../../models" import { Type } from "class-transformer" -import { IInventoryService } from "@medusajs/types" /** * @oas [get] /admin/products diff --git a/packages/medusa/src/api/routes/admin/variants/get-variant.ts b/packages/medusa/src/api/routes/admin/variants/get-variant.ts index bced474b9a..9bb0e86091 100644 --- a/packages/medusa/src/api/routes/admin/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/admin/variants/get-variant.ts @@ -1,4 +1,5 @@ import { PricingService, ProductVariantService } from "../../../../services" + import { FindParams } from "../../../../types/common" /** diff --git a/packages/medusa/src/api/routes/admin/variants/index.ts b/packages/medusa/src/api/routes/admin/variants/index.ts index 2becbbe587..d78823a094 100644 --- a/packages/medusa/src/api/routes/admin/variants/index.ts +++ b/packages/medusa/src/api/routes/admin/variants/index.ts @@ -1,12 +1,12 @@ -import { Router } from "express" - -import { ProductVariant } from "../../../../models/product-variant" -import { PaginatedResponse } from "../../../../types/common" -import { PricedVariant } from "../../../../types/pricing" import middlewares, { transformQuery } from "../../../middlewares" -import { checkRegisteredModules } from "../../../middlewares/check-registered-modules" + import { AdminGetVariantParams } from "./get-variant" import { AdminGetVariantsParams } from "./list-variants" +import { PaginatedResponse } from "../../../../types/common" +import { PricedVariant } from "../../../../types/pricing" +import { ProductVariant } from "../../../../models/product-variant" +import { Router } from "express" +import { checkRegisteredModules } from "../../../middlewares/check-registered-modules" const route = Router() @@ -81,6 +81,8 @@ export const defaultAdminVariantFields: (keyof ProductVariant)[] = [ * - options * - prices * - product + * totals: + * - purchasable * required: * - variants * - count diff --git a/packages/medusa/src/api/routes/store/products/index.ts b/packages/medusa/src/api/routes/store/products/index.ts index d6d84dc6bd..5e448e59ef 100644 --- a/packages/medusa/src/api/routes/store/products/index.ts +++ b/packages/medusa/src/api/routes/store/products/index.ts @@ -1,15 +1,17 @@ -import { Router } from "express" import "reflect-metadata" -import { Product } from "../../../.." -import { PaginatedResponse } from "../../../../types/common" -import { FlagRouter } from "../../../../utils/flag-router" import middlewares, { transformStoreQuery } from "../../../middlewares" + +import { FlagRouter } from "../../../../utils/flag-router" +import { PaginatedResponse } from "../../../../types/common" +import { PricedProduct } from "../../../../types/pricing" +import { Product } from "../../../.." +import { Router } from "express" +import { StoreGetProductsParams } from "./list-products" +import { StoreGetProductsProductParams } from "./get-product" import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params" import { validateProductSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-product-sales-channel-association" import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param" -import { StoreGetProductsProductParams } from "./get-product" -import { StoreGetProductsParams } from "./list-products" const route = Router() @@ -122,6 +124,8 @@ export * from "./search" * - variants * - variants.options * - variants.prices + * totals: + * - variants.purchasable * required: * - product * properties: @@ -129,7 +133,7 @@ export * from "./search" * $ref: "#/components/schemas/PricedProduct" */ export type StoreProductsRes = { - product: Product + product: PricedProduct } /** @@ -163,6 +167,8 @@ export type StorePostSearchRes = { * - variants * - variants.options * - variants.prices + * totals: + * - variants.purchasable * required: * - products * - count @@ -184,5 +190,5 @@ export type StorePostSearchRes = { * description: The number of items per page */ export type StoreProductsListRes = PaginatedResponse & { - products: Product[] + products: PricedProduct[] } diff --git a/packages/medusa/src/api/routes/store/variants/index.ts b/packages/medusa/src/api/routes/store/variants/index.ts index be64fd85b4..492cd4394b 100644 --- a/packages/medusa/src/api/routes/store/variants/index.ts +++ b/packages/medusa/src/api/routes/store/variants/index.ts @@ -1,9 +1,9 @@ import { Router } from "express" -import { PricedVariant } from "../../../../types/pricing" -import middlewares from "../../../middlewares" import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params" -import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param" +import middlewares from "../../../middlewares" import { validateProductVariantSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-variant-sales-channel-association" +import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param" +import { PricedVariant } from "../../../../types/pricing" const route = Router() @@ -29,6 +29,8 @@ export const defaultStoreVariantRelations = ["prices", "options", "product"] * - prices * - options * - product + * totals: + * - purchasable * required: * - variant * properties: @@ -48,6 +50,8 @@ export type StoreVariantsRes = { * - prices * - options * - product + * totals: + * - purchasable * required: * - variants * properties: diff --git a/packages/medusa/src/models/product-variant.ts b/packages/medusa/src/models/product-variant.ts index d65def55ce..e1e88c6a1c 100644 --- a/packages/medusa/src/models/product-variant.ts +++ b/packages/medusa/src/models/product-variant.ts @@ -1,8 +1,3 @@ -import { DbAwareColumn, generateEntityId } from "../utils" -import { MoneyAmount } from "./money-amount" -import { Product } from "./product" -import { ProductOptionValue } from "./product-option-value" -import { SoftDeletableEntity } from "../interfaces" import { BeforeInsert, Column, @@ -10,10 +5,15 @@ import { Index, JoinColumn, ManyToOne, - OneToMany + OneToMany, } from "typeorm" +import { DbAwareColumn, generateEntityId } from "../utils" +import { MoneyAmount } from "./money-amount" +import { Product } from "./product" +import { ProductOptionValue } from "./product-option-value" import { ProductVariantInventoryItem } from "./product-variant-inventory-item" +import { SoftDeletableEntity } from "../interfaces" @Entity() export class ProductVariant extends SoftDeletableEntity { @@ -103,6 +103,8 @@ export class ProductVariant extends SoftDeletableEntity { @DbAwareColumn({ type: "jsonb", nullable: true }) metadata: Record + purchasable?: boolean + @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "variant") @@ -264,4 +266,14 @@ export class ProductVariant extends SoftDeletableEntity { * nullable: true * type: object * example: {car: "white"} + * purchasable: + * description: | + * Only used with the inventory modules. + * A boolean value indicating whether the Product Variant is purchasable. + * A variant is purchasable if: + * - inventory is not managed + * - it has no inventory items + * - it is in stock + * - it is backorderable. + * type: boolean */ diff --git a/packages/medusa/src/services/product-variant-inventory.ts b/packages/medusa/src/services/product-variant-inventory.ts index 5055b0041d..cee2017248 100644 --- a/packages/medusa/src/services/product-variant-inventory.ts +++ b/packages/medusa/src/services/product-variant-inventory.ts @@ -638,28 +638,50 @@ class ProductVariantInventoryService extends TransactionBaseService { async setVariantAvailability( variants: ProductVariant[] | PricedVariant[], - salesChannelId: string | string[] | undefined + salesChannelId: string | string[] | undefined, + variantInventoryMap: Map = new Map() ): Promise { if (!this.inventoryService_) { return variants } + if (!variantInventoryMap.size) { + const variantInventories = await this.listByVariant( + variants.map((v) => v.id) + ) + + variantInventories.forEach((inventory) => { + const variantId = inventory.variant_id + const currentInventories = variantInventoryMap.get(variantId) || [] + currentInventories.push(inventory) + variantInventoryMap.set(variantId, currentInventories) + }) + } + return await Promise.all( variants.map(async (variant) => { if (!variant.id) { return variant } - if (!salesChannelId) { - delete variant.inventory_quantity + variant.purchasable = variant.allow_backorder + + if (!variant.manage_inventory) { + variant.purchasable = true return variant } - // first get all inventory items required for a variant - const variantInventory = await this.listByVariant(variant.id) + const variantInventory = variantInventoryMap.get(variant.id) || [] if (!variantInventory.length) { - variant.inventory_quantity = 0 + delete variant.inventory_quantity + variant.purchasable = true + return variant + } + + if (!salesChannelId) { + delete variant.inventory_quantity + variant.purchasable = false return variant } @@ -678,6 +700,9 @@ class ProductVariantInventoryService extends TransactionBaseService { 0 ) + variant.purchasable = + variant.inventory_quantity > 0 || variant.allow_backorder + return variant }) ) diff --git a/packages/medusa/src/types/pricing.ts b/packages/medusa/src/types/pricing.ts index 9faaba1758..183bb8ae04 100644 --- a/packages/medusa/src/types/pricing.ts +++ b/packages/medusa/src/types/pricing.ts @@ -1,5 +1,6 @@ -import { PriceSelectionContext } from "../interfaces/price-selection-strategy" import { MoneyAmount, Product, ProductVariant, ShippingOption } from "../models" + +import { PriceSelectionContext } from "../interfaces/price-selection-strategy" import { TaxServiceRate } from "./tax-service" export type ProductVariantPricing = { diff --git a/packages/medusa/src/types/product-variant.ts b/packages/medusa/src/types/product-variant.ts index 3a09841679..15a429be48 100644 --- a/packages/medusa/src/types/product-variant.ts +++ b/packages/medusa/src/types/product-variant.ts @@ -1,3 +1,9 @@ +import { + DateComparisonOperator, + NumericalComparisonOperator, + StringComparisonOperator, + WithRequiredProperty, +} from "./common" import { IsBoolean, IsInt, @@ -7,15 +13,11 @@ import { ValidateIf, ValidateNested, } from "class-validator" + import { IsType } from "../utils/validators/is-type" -import { - DateComparisonOperator, - NumericalComparisonOperator, - StringComparisonOperator, - WithRequiredProperty, -} from "./common" -import { XorConstraint } from "./validators/xor" +import { PricedVariant } from "./pricing" import { ProductVariant } from "../models" +import { XorConstraint } from "./validators/xor" export type ProductVariantPrice = { id?: string diff --git a/packages/medusa/src/types/product.ts b/packages/medusa/src/types/product.ts index f1de3898ac..59d51fabdb 100644 --- a/packages/medusa/src/types/product.ts +++ b/packages/medusa/src/types/product.ts @@ -1,4 +1,4 @@ -import { Transform, Type } from "class-transformer" +import { DateComparisonOperator, FindConfig, Selector } from "./common" import { IsArray, IsBoolean, @@ -7,21 +7,22 @@ import { IsString, ValidateNested, } from "class-validator" -import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { PriceList, Product, + ProductCategory, ProductOptionValue, ProductStatus, SalesChannel, - ProductCategory, } from "../models" +import { Transform, Type } from "class-transformer" + import { FeatureFlagDecorators } from "../utils/feature-flag-decorators" -import { optionalBooleanMapper } from "../utils/validators/is-boolean" -import { IsType } from "../utils/validators/is-type" -import { DateComparisonOperator, FindConfig, Selector } from "./common" -import { PriceListLoadConfig } from "./price-list" import { FindOperator } from "typeorm" +import { IsType } from "../utils/validators/is-type" +import { PriceListLoadConfig } from "./price-list" +import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" +import { optionalBooleanMapper } from "../utils/validators/is-boolean" /** * API Level DTOs + Validation rules