feat(medusa): Add purchasable prop on variants when setting availability (#3811)
* write integration tests * update variant inventory decorator * update types * add changeset * feedback comments * add yaml schemas * different oas approach * pr feedback * update oas --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
5
.changeset/plenty-cats-thank.md
Normal file
5
.changeset/plenty-cats-thank.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
fix(medusa): add purchasable property
|
||||
10
docs/api/admin/components/schemas/DecoratedProduct.yaml
Normal file
10
docs/api/admin/components/schemas/DecoratedProduct.yaml
Normal file
@@ -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
|
||||
9
docs/api/admin/components/schemas/DecoratedVariant.yaml
Normal file
9
docs/api/admin/components/schemas/DecoratedVariant.yaml
Normal file
@@ -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.
|
||||
10
docs/api/store/components/schemas/DecoratedProduct.yaml
Normal file
10
docs/api/store/components/schemas/DecoratedProduct.yaml
Normal file
@@ -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
|
||||
9
docs/api/store/components/schemas/DecoratedVariant.yaml
Normal file
9
docs/api/store/components/schemas/DecoratedVariant.yaml
Normal file
@@ -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.
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,7 +14,9 @@ export interface AdminProductsListRes {
|
||||
"collection" | "images" | "options" | "tags" | "type" | "variants"
|
||||
>,
|
||||
{
|
||||
variants: Array<SetRelation<ProductVariant, "options" | "prices">>
|
||||
variants: Array<
|
||||
SetRelation<ProductVariant, "options" | "prices" | "purchasable">
|
||||
>
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
@@ -6,7 +6,9 @@ import { SetRelation, Merge } from "../core/ModelUtils"
|
||||
import type { PricedVariant } from "./PricedVariant"
|
||||
|
||||
export interface AdminVariantsListRes {
|
||||
variants: Array<SetRelation<PricedVariant, "options" | "prices" | "product">>
|
||||
variants: Array<
|
||||
SetRelation<PricedVariant, "options" | "prices" | "product" | "purchasable">
|
||||
>
|
||||
/**
|
||||
* The total number of items available
|
||||
*/
|
||||
|
||||
@@ -120,4 +120,15 @@ export interface ProductVariant {
|
||||
* An optional key-value map with additional details
|
||||
*/
|
||||
metadata: Record<string, any> | 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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ export interface StoreProductsListRes {
|
||||
>,
|
||||
{
|
||||
options: Array<SetRelation<ProductOption, "values">>
|
||||
variants: Array<SetRelation<ProductVariant, "options" | "prices">>
|
||||
variants: Array<
|
||||
SetRelation<ProductVariant, "options" | "prices" | "purchasable">
|
||||
>
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
@@ -15,7 +15,9 @@ export interface StoreProductsRes {
|
||||
>,
|
||||
{
|
||||
options: Array<SetRelation<ProductOption, "values">>
|
||||
variants: Array<SetRelation<ProductVariant, "options" | "prices">>
|
||||
variants: Array<
|
||||
SetRelation<ProductVariant, "options" | "prices" | "purchasable">
|
||||
>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
@@ -6,5 +6,7 @@ import { SetRelation, Merge } from "../core/ModelUtils"
|
||||
import type { PricedVariant } from "./PricedVariant"
|
||||
|
||||
export interface StoreVariantsListRes {
|
||||
variants: Array<SetRelation<PricedVariant, "prices" | "options" | "product">>
|
||||
variants: Array<
|
||||
SetRelation<PricedVariant, "prices" | "options" | "product" | "purchasable">
|
||||
>
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@ import { SetRelation, Merge } from "../core/ModelUtils"
|
||||
import type { PricedVariant } from "./PricedVariant"
|
||||
|
||||
export interface StoreVariantsRes {
|
||||
variant: SetRelation<PricedVariant, "prices" | "options" | "product">
|
||||
variant: SetRelation<
|
||||
PricedVariant,
|
||||
"prices" | "options" | "product" | "purchasable"
|
||||
>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PricingService, ProductVariantService } from "../../../../services"
|
||||
|
||||
import { FindParams } from "../../../../types/common"
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<string, unknown>
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -638,28 +638,50 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
||||
|
||||
async setVariantAvailability(
|
||||
variants: ProductVariant[] | PricedVariant[],
|
||||
salesChannelId: string | string[] | undefined
|
||||
salesChannelId: string | string[] | undefined,
|
||||
variantInventoryMap: Map<string, ProductVariantInventoryItem[]> = new Map()
|
||||
): Promise<ProductVariant[] | PricedVariant[]> {
|
||||
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
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user