feat(medusa): Add inventory for variants (#2970)

* initial get-inventory impl

* add inventory management to create-variant

* update create-variant endpoint

* move if statement

* use middleware for module checking

* add endpoint to medusa-js

* export return type from get-inventory

* add jsdoc

* revert create variant

* rename variable

* initial setInventoryPattern for variant and product endpoints

* remove cache

* sort imports

* add sales channel info to inventory calculations

* add missing import

* remove promise.all from single promise

* update oas

* initial feedback

* checkout joinLevels from develop

* add variant middleware

* add comments
This commit is contained in:
Philip Korsholm
2023-01-24 11:10:33 +01:00
committed by GitHub
parent 09dc9c6677
commit 8f4c84121b
13 changed files with 495 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
Feat(medusa): Add multi warehouse to create-variant and add list-inventory for variant endpoints
@@ -1,9 +1,19 @@
import { AdminGetVariantsParams, AdminVariantsListRes } from "@medusajs/medusa"
import {
AdminGetVariantsVariantInventoryRes,
AdminGetVariantsParams,
AdminVariantsListRes,
} from "@medusajs/medusa"
import qs from "qs"
import { ResponsePromise } from "../.."
import BaseResource from "../base"
class AdminVariantsResource extends BaseResource {
/**
* List product variants
* @param query Query to filter variants by
* @param customHeaders custom headers
* @returns A list of variants satisfying the criteria of the query
*/
list(
query?: AdminGetVariantsParams,
customHeaders: Record<string, any> = {}
@@ -17,6 +27,21 @@ class AdminVariantsResource extends BaseResource {
return this.client.request("GET", path, undefined, {}, customHeaders)
}
/**
*
* @param variantId id of the variant to fetch inventory for
* @param customHeaders custom headers
* @returns
*/
getInventory(
variantId: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminGetVariantsVariantInventoryRes> {
const path = `/admin/variants/${variantId}/inventory`
return this.client.request("GET", path, undefined, {}, customHeaders)
}
}
export default AdminVariantsResource
@@ -0,0 +1,48 @@
import { NextFunction, Request, Response } from "express"
import PublishableApiKeyService from "../../../services/publishable-api-key"
import { ProductService, ProductVariantService } from "../../../services"
/**
* The middleware check if requested product is assigned to a SC associated with PK in the header.
*
* @param req - request object
* @param res - response object
* @param next - next middleware call
*/
async function validateProductVariantSalesChannelAssociation(
req: Request,
res: Response,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const productVariantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const publishableKeyService: PublishableApiKeyService = req.scope.resolve(
"publishableApiKeyService"
)
const { sales_channel_id: salesChannelIds } =
await publishableKeyService.getResourceScopes(pubKey)
if (
salesChannelIds.length &&
!(await productVariantService.isVariantInSalesChannels(
req.params.id,
salesChannelIds
))
) {
req.errors = req.errors ?? []
req.errors.push(
`Variant with id: ${req.params.id} is not associated with sales channels defined by the Publishable API Key passed in the header of the request.`
)
}
}
next()
}
export { validateProductVariantSalesChannelAssociation }
@@ -0,0 +1,180 @@
import {
InventoryItemDTO,
InventoryLevelDTO,
} from "../../../../types/inventory"
import ProductVariantInventoryService from "../../../../services/product-variant-inventory"
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../services"
import { SalesChannel } from "../../../../models"
import { IInventoryService } from "../../../../interfaces"
import ProductVariantService from "../../../../services/product-variant"
import { joinLevels } from "../inventory-items/utils/join-levels"
/**
* @oas [get] /variants/{id}/inventory
* operationId: "GetVariantsVariantInventory"
* summary: "Get inventory of Variant."
* description: "Returns the available inventory of a Variant."
* x-authenticated: true
* parameters:
* - (path) id {string} The Product Variant id to get inventory for.
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.variants.list()
* .then(({ variants, limit, offset, count }) => {
* console.log(variants.length)
* })
* - lang: Shell
* label: cURL
* source: |
* curl --location --request GET 'https://medusa-url.com/admin/variants' \
* --header 'Authorization: Bearer {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - Product Variant
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* variant:
* type: object
* $ref: "#/components/schemas/AdminGetVariantsVariantInventoryRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const inventoryService: IInventoryService =
req.scope.resolve("inventoryService")
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const channelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const variantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const variant = await variantService.retrieve(id, { select: ["id"] })
const responseVariant: AdminGetVariantsVariantInventoryRes = {
id: variant.id,
inventory: [],
sales_channel_availability: [],
}
const [rawChannels] = await channelService.listAndCount({})
const channels: SalesChannelDTO[] = await Promise.all(
rawChannels.map(async (channel) => {
const locations = await channelLocationService.listLocations(channel.id)
return {
...channel,
locations,
}
})
)
const inventory =
await productVariantInventoryService.listInventoryItemsByVariant(variant.id)
responseVariant.inventory = await joinLevels(inventory, [], inventoryService)
// TODO: adjust for required quantity
if (inventory.length) {
responseVariant.sales_channel_availability = await Promise.all(
channels.map(async (channel) => {
if (!channel.locations.length) {
return {
channel_name: channel.name as string,
channel_id: channel.id as string,
available_quantity: 0,
}
}
const quantity = await inventoryService.retrieveAvailableQuantity(
inventory[0].id,
channel.locations
)
return {
channel_name: channel.name as string,
channel_id: channel.id as string,
available_quantity: quantity,
}
})
)
}
res.json({
variant: responseVariant,
})
}
type SalesChannelDTO = Omit<SalesChannel, "beforeInsert"> & {
locations: string[]
}
type ResponseInventoryItem = Partial<InventoryItemDTO> & {
location_levels?: InventoryLevelDTO[]
}
/**
* @schema AdminGetVariantsVariantInventoryRes
* type: object
* properties:
* id:
* description: the id of the variant
* type: string
* inventory:
* description: the stock location address ID
* type: string
* sales_channel_availability:
* type: object
* description: An optional key-value map with additional details
* properties:
* channel_name:
* description: Sales channel name
* type: string
* channel_id:
* description: Sales channel id
* type: string
* available_quantity:
* description: Available quantity in sales channel
* type: number
*/
export type AdminGetVariantsVariantInventoryRes = {
id: string
inventory: ResponseInventoryItem[]
sales_channel_availability: {
channel_name: string
channel_id: string
available_quantity: number
}[]
}
@@ -4,6 +4,7 @@ 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 { AdminGetVariantsParams } from "./list-variants"
const route = Router()
@@ -21,6 +22,15 @@ export default (app) => {
middlewares.wrap(require("./list-variants").default)
)
route.get(
"/:id/inventory",
checkRegisteredModules({
inventoryService:
"Inventory is not enabled. Please add an Inventory module to enable this functionality.",
}),
middlewares.wrap(require("./get-inventory").default)
)
return app
}
@@ -72,3 +82,4 @@ export type AdminVariantsListRes = PaginatedResponse & {
}
export * from "./list-variants"
export * from "./get-inventory"
@@ -1,11 +1,15 @@
import { IsOptional, IsString } from "class-validator"
import { defaultStoreProductsRelations } from "."
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import {
CartService,
PricingService,
ProductService,
ProductVariantInventoryService,
RegionService,
} from "../../../../services"
import { PriceSelectionParams } from "../../../../types/price-selection"
import { FlagRouter } from "../../../../utils/flag-router"
import { validator } from "../../../../utils/validator"
/**
@@ -15,6 +19,7 @@ import { validator } from "../../../../utils/validator"
* description: "Retrieves a Product."
* parameters:
* - (path) id=* {string} The id of the Product.
* - (query) sales_channel_id {string} The sales channel used when fetching the product.
* - (query) cart_id {string} The ID of the customer's cart.
* - (query) region_id {string} The ID of the region the customer is using. This is helpful to ensure correct prices are retrieved for a region.
* - in: query
@@ -71,6 +76,8 @@ export default async (req, res) => {
const customer_id = req.user?.customer_id
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const productService: ProductService = req.scope.resolve("productService")
const pricingService: PricingService = req.scope.resolve("pricingService")
const cartService: CartService = req.scope.resolve("cartService")
@@ -79,6 +86,14 @@ export default async (req, res) => {
relations: defaultStoreProductsRelations,
})
let sales_channel_id = validated.sales_channel_id
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
if (req.publishableApiKeyScopes?.sales_channel_id.length === 1) {
sales_channel_id = req.publishableApiKeyScopes.sales_channel_id[0]
}
}
let regionId = validated.region_id
let currencyCode = validated.currency_code
if (validated.cart_id) {
@@ -92,15 +107,27 @@ export default async (req, res) => {
currencyCode = region.currency_code
}
const [product] = await pricingService.setProductPrices([rawProduct], {
cart_id: validated.cart_id,
customer_id: customer_id,
region_id: regionId,
currency_code: currencyCode,
include_discount_prices: true,
})
const pricedProductArray = await pricingService.setProductPrices(
[rawProduct],
{
cart_id: validated.cart_id,
customer_id: customer_id,
region_id: regionId,
currency_code: currencyCode,
include_discount_prices: true,
}
)
const [product] = await productVariantInventoryService.setProductAvailability(
pricedProductArray,
sales_channel_id
)
res.json({ product })
}
export class StoreGetProductsProductParams extends PriceSelectionParams {}
export class StoreGetProductsProductParams extends PriceSelectionParams {
@IsString()
@IsOptional()
sales_channel_id?: string
}
@@ -10,6 +10,7 @@ import {
import {
CartService,
ProductService,
ProductVariantInventoryService,
RegionService,
} from "../../../../services"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
@@ -171,6 +172,8 @@ import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/pub
*/
export default async (req, res) => {
const productService: ProductService = req.scope.resolve("productService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const pricingService: PricingService = req.scope.resolve("pricingService")
const cartService: CartService = req.scope.resolve("cartService")
const regionService: RegionService = req.scope.resolve("regionService")
@@ -214,7 +217,7 @@ export default async (req, res) => {
currencyCode = region.currency_code
}
const products = await pricingService.setProductPrices(rawProducts, {
const pricedProducts = await pricingService.setProductPrices(rawProducts, {
cart_id: cart_id,
region_id: regionId,
currency_code: currencyCode,
@@ -222,6 +225,11 @@ export default async (req, res) => {
include_discount_prices: true,
})
const products = await productVariantInventoryService.setProductAvailability(
pricedProducts,
filterableFields.sales_channel_id
)
res.json({
products,
count,
@@ -1,6 +1,7 @@
import {
CartService,
PricingService,
ProductVariantInventoryService,
ProductVariantService,
RegionService,
} from "../../../../services"
@@ -8,6 +9,9 @@ import {
import { PriceSelectionParams } from "../../../../types/price-selection"
import { defaultStoreVariantRelations } from "."
import { validator } from "../../../../utils/validator"
import { IsOptional, IsString } from "class-validator"
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import { FlagRouter } from "../../../../utils/flag-router"
/**
* @oas [get] /variants/{variant_id}
@@ -17,6 +21,7 @@ import { validator } from "../../../../utils/validator"
* parameters:
* - (path) variant_id=* {string} The id of the Product Variant.
* - (query) cart_id {string} The id of the Cart to set prices based on.
* - (query) sales_channel_id {string} A sales channel id for result configuration.
* - (query) region_id {string} The id of the Region to set prices based on.
* - in: query
* name: currency_code
@@ -65,6 +70,8 @@ export default async (req, res) => {
"productVariantService"
)
const pricingService: PricingService = req.scope.resolve("pricingService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const cartService: CartService = req.scope.resolve("cartService")
const regionService: RegionService = req.scope.resolve("regionService")
@@ -74,6 +81,14 @@ export default async (req, res) => {
relations: defaultStoreVariantRelations,
})
let sales_channel_id = validated.sales_channel_id
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
if (req.publishableApiKeyScopes?.sales_channel_id.length === 1) {
sales_channel_id = req.publishableApiKeyScopes.sales_channel_id[0]
}
}
let regionId = validated.region_id
let currencyCode = validated.currency_code
if (validated.cart_id) {
@@ -87,7 +102,7 @@ export default async (req, res) => {
currencyCode = region.currency_code
}
const [variant] = await pricingService.setVariantPrices([rawVariant], {
const variantRes = await pricingService.setVariantPrices([rawVariant], {
cart_id: validated.cart_id,
customer_id: customer_id,
region_id: regionId,
@@ -95,7 +110,16 @@ export default async (req, res) => {
include_discount_prices: true,
})
const [variant] = await productVariantInventoryService.setVariantAvailability(
variantRes,
sales_channel_id
)
res.json({ variant })
}
export class StoreGetVariantsVariantParams extends PriceSelectionParams {}
export class StoreGetVariantsVariantParams extends PriceSelectionParams {
@IsString()
@IsOptional()
sales_channel_id?: string
}
@@ -1,12 +1,26 @@
import { ProductVariant } from "../../../../"
import { Router } from "express"
import { RequestHandler, Router } from "express"
import middlewares from "../../../middlewares"
import { featureFlagRouter } from "../../../../loaders/feature-flags"
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params"
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
import { validateProductVariantSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-variant-sales-channel-association"
const route = Router()
export default (app) => {
app.use("/variants", route)
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
route.use(
"/",
extendRequestParams as unknown as RequestHandler,
validateSalesChannelParam as unknown as RequestHandler
)
route.use("/:id", validateProductVariantSalesChannelAssociation)
}
route.get("/", middlewares.wrap(require("./list-variants").default))
route.get("/:id", middlewares.wrap(require("./get-variant").default))
@@ -2,6 +2,7 @@ import { IsInt, IsOptional, IsString } from "class-validator"
import {
CartService,
PricingService,
ProductVariantInventoryService,
ProductVariantService,
RegionService,
} from "../../../../services"
@@ -14,6 +15,8 @@ import { PriceSelectionParams } from "../../../../types/price-selection"
import { FilterableProductVariantProps } from "../../../../types/product-variant"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import { FlagRouter } from "../../../../utils/flag-router"
/**
* @oas [get] /variants
@@ -22,6 +25,7 @@ import { IsType } from "../../../../utils/validators/is-type"
* description: "Retrieves a list of Product Variants"
* parameters:
* - (query) ids {string} A comma separated list of Product Variant ids to filter by.
* - (query) sales_channel_id {string} A sales channel id for result configuration.
* - (query) expand {string} A comma separated list of Product Variant relations to load.
* - (query) offset=0 {number} How many product variants to skip in the result.
* - (query) limit=100 {number} Maximum number of Product Variants to return.
@@ -124,11 +128,21 @@ export default async (req, res) => {
filterableFields.id = validated.ids.split(",")
}
let sales_channel_id = validated.sales_channel_id
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
if (req.publishableApiKeyScopes?.sales_channel_id.length === 1) {
sales_channel_id = req.publishableApiKeyScopes.sales_channel_id[0]
}
}
const pricingService: PricingService = req.scope.resolve("pricingService")
const variantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const regionService: RegionService = req.scope.resolve("regionService")
const rawVariants = await variantService.list(filterableFields, listConfig)
@@ -146,7 +160,7 @@ export default async (req, res) => {
currencyCode = region.currency_code
}
const variants = await pricingService.setVariantPrices(rawVariants, {
const pricedVariants = await pricingService.setVariantPrices(rawVariants, {
cart_id: validated.cart_id,
region_id: regionId,
currency_code: currencyCode,
@@ -154,6 +168,11 @@ export default async (req, res) => {
include_discount_prices: true,
})
const variants = await productVariantInventoryService.setVariantAvailability(
pricedVariants,
sales_channel_id
)
res.json({ variants })
}
@@ -176,6 +195,10 @@ export class StoreGetVariantsParams extends PriceSelectionParams {
@IsString()
ids?: string
@IsOptional()
@IsString()
sales_channel_id?: string
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
@@ -22,6 +22,16 @@ export const ProductVariantInventoryServiceMock = {
validateInventoryAtLocation: jest
.fn()
.mockImplementation((items, locationId) => {}),
setVariantAvailability: jest
.fn()
.mockImplementation((variants, salesChannelId) => {
return variants
}),
setProductAvailability: jest
.fn()
.mockImplementation((products, salesChannelId) => {
return products
}),
}
const mock = jest.fn().mockImplementation(() => {
@@ -1,22 +1,29 @@
import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager, In } from "typeorm"
import {
IStockLocationService,
IInventoryService,
IStockLocationService,
TransactionBaseService,
} from "../interfaces"
import { LineItem, Product, ProductVariant } from "../models"
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
import { ProductVariantService, SalesChannelLocationService } from "./"
import {
InventoryItemDTO,
ReservationItemDTO,
ReserveQuantityContext,
} from "../types/inventory"
import { LineItem, ProductVariant } from "../models"
import { PricedProduct, PricedVariant } from "../types/pricing"
import {
CacheService,
ProductVariantService,
SalesChannelInventoryService,
SalesChannelLocationService,
} from "./"
type InjectedDependencies = {
manager: EntityManager
salesChannelLocationService: SalesChannelLocationService
salesChannelInventoryService: SalesChannelInventoryService
productVariantService: ProductVariantService
stockLocationService: IStockLocationService
inventoryService: IInventoryService
@@ -27,14 +34,17 @@ class ProductVariantInventoryService extends TransactionBaseService {
protected transactionManager_: EntityManager | undefined
protected readonly salesChannelLocationService_: SalesChannelLocationService
protected readonly salesChannelInventoryService_: SalesChannelInventoryService
protected readonly productVariantService_: ProductVariantService
protected readonly stockLocationService_: IStockLocationService
protected readonly inventoryService_: IInventoryService
protected readonly cacheService_: CacheService
constructor({
manager,
stockLocationService,
salesChannelLocationService,
salesChannelInventoryService,
productVariantService,
inventoryService,
}: InjectedDependencies) {
@@ -43,6 +53,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
this.manager_ = manager
this.salesChannelLocationService_ = salesChannelLocationService
this.salesChannelInventoryService_ = salesChannelInventoryService
this.stockLocationService_ = stockLocationService
this.productVariantService_ = productVariantService
this.inventoryService_ = inventoryService
@@ -598,6 +609,74 @@ class ProductVariantInventoryService extends TransactionBaseService {
)
}
}
async setVariantAvailability(
variants: ProductVariant[] | PricedVariant[],
salesChannelId: string | undefined
): Promise<ProductVariant[] | PricedVariant[]> {
if (!this.inventoryService_) {
return variants
}
return await Promise.all(
variants.map(async (variant) => {
if (!variant.id) {
return variant
}
if (!salesChannelId) {
delete variant.inventory_quantity
return variant
}
// first get all inventory items required for a variant
const variantInventory = await this.listByVariant(variant.id)
// the inventory quantity of the variant should be equal to the inventory
// item with the smallest stock, adjusted for quantity required to fulfill
// the given variant
variant.inventory_quantity = Math.min(
...(await Promise.all(
variantInventory.map(async (variantInventory) => {
// get the total available quantity for the given sales channel
// divided by the required quantity to account for how many of the
// variant we can fulfill at the current time. Take the minimum we
// can fulfill and set that as quantity
return (
// eslint-disable-next-line max-len
(await this.salesChannelInventoryService_.retrieveAvailableItemQuantity(
salesChannelId,
variantInventory.inventory_item_id
)) / variantInventory.required_quantity
)
})
))
)
return variant
})
)
}
async setProductAvailability(
products: (Product | PricedProduct)[],
salesChannelId: string | undefined
): Promise<(Product | PricedProduct)[]> {
return await Promise.all(
products.map(async (product) => {
if (!product.variants || product.variants.length === 0) {
return product
}
product.variants = await this.setVariantAvailability(
product.variants,
salesChannelId
)
return product
})
)
}
}
export default ProductVariantInventoryService
@@ -47,6 +47,7 @@ class ProductVariantService extends TransactionBaseService {
protected readonly regionService_: RegionService
protected readonly priceSelectionStrategy_: IPriceSelectionStrategy
protected readonly moneyAmountRepository_: typeof MoneyAmountRepository
// eslint-disable-next-line max-len
protected readonly productOptionValueRepository_: typeof ProductOptionValueRepository
protected readonly cartRepository_: typeof CartRepository
@@ -61,6 +62,7 @@ class ProductVariantService extends TransactionBaseService {
cartRepository,
priceSelectionStrategy,
}) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.manager_ = manager
@@ -675,6 +677,28 @@ class ProductVariantService extends TransactionBaseService {
})
}
/**
* Check if the variant is assigned to at least one of the provided sales channels.
*
* @param id - product variant id
* @param salesChannelIds - an array of sales channel ids
*/
async isVariantInSalesChannels(
id: string,
salesChannelIds: string[]
): Promise<boolean> {
const variant = await this.retrieve(id, {
relations: ["product", "product.sales_channels"],
})
// TODO: reimplement this to use db level check
const productsSalesChannels = variant.product.sales_channels.map(
(channel) => channel.id
)
return productsSalesChannels.some((id) => salesChannelIds.includes(id))
}
/**
* Creates a query object to be used for list queries.
* @param selector - the selector to create the query from