feat(medusa,types,workflows,utils,product): PricingModule Integration of PriceLists into Core (#5536)

This commit is contained in:
Philip Korsholm authored and GitHub committed 2023-11-21 17:42:37 +00:00
1 parent 9c7ac7332a
commit dc5750dd66
94 files changed
+5697 -880

No files matched your search

@@ -1,12 +1,15 @@
import { MedusaV2Flag } from "@medusajs/utils"
import { updatePriceLists } from "@medusajs/workflows"
import { Type } from "class-transformer"
import { IsArray, IsBoolean, IsOptional, ValidateNested } from "class-validator"
import { EntityManager } from "typeorm"
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list"
import { PriceList } from "../../../.."
import PriceListService from "../../../../services/price-list"
import { Type } from "class-transformer"
import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list"
import { validator } from "../../../../utils/validator"
import { EntityManager } from "typeorm"
import { MedusaContainer } from "@medusajs/types"
import { getPriceListPricingModule } from "./modules-queries"
/**
* @oas [post] /admin/price-lists/{id}/prices/batch
@@ -85,23 +88,48 @@ import { EntityManager } from "typeorm"
*/
export default async (req, res) => {
const { id } = req.params
const validated = await validator(AdminPostPriceListPricesPricesReq, req.body)
let priceList
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.addPrices(id, validated.prices, validated.override)
})
const validated = await validator(AdminPostPriceListPricesPricesReq, req.body)
const priceList = await priceListService.retrieve(id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const updatePriceListWorkflow = updatePriceLists(req.scope)
const input = {
price_lists: [
{
id,
...validated,
},
],
}
await updatePriceListWorkflow.run({
input,
context: {
manager,
},
})
priceList = await getPriceListPricingModule(id, {
container: req.scope as MedusaContainer,
})
} else {
await manager.transaction(async (transactionManager) => {
await priceListService
.withTransaction(transactionManager)
.addPrices(id, validated.prices, validated.override)
})
priceList = await priceListService.retrieve(id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
}
res.json({ price_list: priceList })
}
@@ -1,3 +1,12 @@
import { MedusaContainer, PricingTypes, WorkflowTypes } from "@medusajs/types"
import {
FlagRouter,
MedusaV2Flag,
PriceListStatus,
PriceListType,
} from "@medusajs/utils"
import { createPriceLists } from "@medusajs/workflows"
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
@@ -6,23 +15,18 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { Request } from "express"
import { EntityManager } from "typeorm"
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { PriceList } from "../../../../models"
import PriceListService from "../../../../services/price-list"
import {
AdminPriceListPricesCreateReq,
CreatePriceListInput,
} from "../../../../types/price-list"
import { PriceListStatus, PriceListType } from "@medusajs/utils"
import { Type } from "class-transformer"
import { Request } from "express"
import { EntityManager } from "typeorm"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { PriceList } from "../../../../models"
import PriceListService from "../../../../services/price-list"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import {
defaultAdminPriceListFields,
defaultAdminPriceListRelations,
} from "./index"
import { getPriceListPricingModule } from "./modules-queries"
/**
* @oas [post] /admin/price-lists
@@ -109,16 +113,57 @@ export default async (req: Request, res) => {
req.scope.resolve("priceListService")
const manager: EntityManager = req.scope.resolve("manager")
let priceList = await manager.transaction(async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.create(req.validatedBody as CreatePriceListInput)
})
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
let priceList
priceList = await priceListService.retrieve(priceList.id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
if (isMedusaV2FlagEnabled) {
const createPriceListWorkflow = createPriceLists(req.scope)
const validatedInput = req.validatedBody as CreatePriceListInput
const rules: PricingTypes.CreatePriceListRules = {}
const customerGroups = validatedInput?.customer_groups || []
delete validatedInput.customer_groups
if (customerGroups.length) {
rules["customer_group_id"] = customerGroups.map((cg) => cg.id)
}
const input = {
price_lists: [
{
...validatedInput,
rules,
},
],
} as WorkflowTypes.PriceListWorkflow.CreatePriceListWorkflowInputDTO
const { result } = await createPriceListWorkflow.run({
input,
context: {
manager,
},
})
priceList = result[0]!.priceList
priceList = await getPriceListPricingModule(priceList.id, {
container: req.scope as MedusaContainer,
})
} else {
priceList = await manager.transaction(async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.create(req.validatedBody as CreatePriceListInput)
})
priceList = await priceListService.retrieve(priceList.id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
}
res.json({ price_list: priceList })
}
@@ -1,3 +1,6 @@
import { WorkflowTypes } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { removePriceLists } from "@medusajs/workflows"
import { EntityManager } from "typeorm"
import PriceListService from "../../../../services/price-list"
@@ -56,12 +59,33 @@ import PriceListService from "../../../../services/price-list"
export default async (req, res) => {
const { id } = req.params
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await priceListService.withTransaction(transactionManager).delete(id)
})
const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
if (isMedusaV2FlagEnabled) {
const removePriceListsWorkflow = removePriceLists(req.scope)
const input = {
price_lists: [id],
} as WorkflowTypes.PriceListWorkflow.RemovePriceListWorkflowInputDTO
await removePriceListsWorkflow.run({
input,
context: {
manager,
},
})
} else {
const priceListService: PriceListService =
req.scope.resolve("priceListService")
await manager.transaction(async (transactionManager) => {
await priceListService.withTransaction(transactionManager).delete(id)
})
}
res.json({
id,
@@ -1,8 +1,10 @@
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { removePriceListProductPrices } from "@medusajs/workflows/dist/definition/price-list/remove-price-list-prices"
import { ArrayNotEmpty, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import PriceListService from "../../../../services/price-list"
import { validator } from "../../../../utils/validator"
import { WorkflowTypes } from "@medusajs/types"
/**
* @oas [delete] /admin/price-lists/{id}/prices/batch
@@ -81,13 +83,37 @@ export default async (req, res) => {
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deletePrices(id, validated.price_ids)
})
const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
if (isMedusaV2FlagEnabled) {
const deletePriceListPricesWorkflow = removePriceListProductPrices(
req.scope
)
const input = {
price_list_id: id,
money_amount_ids: validated.price_ids,
} as WorkflowTypes.PriceListWorkflow.RemovePriceListPricesWorkflowInputDTO
await deletePriceListPricesWorkflow.run({
input,
context: {
manager,
},
})
} else {
await manager.transaction(async (transactionManager) => {
await priceListService
.withTransaction(transactionManager)
.deletePrices(id, validated.price_ids)
})
}
res.json({ ids: validated.price_ids, object: "money-amount", deleted: true })
}
@@ -1,3 +1,6 @@
import { WorkflowTypes } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { removePriceListProductPrices } from "@medusajs/workflows/dist/definition/price-list/remove-product-prices"
import { EntityManager } from "typeorm"
import PriceListService from "../../../../services/price-list"
@@ -60,15 +63,44 @@ export default async (req, res) => {
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
const [deletedPriceIds] = await manager.transaction(
async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deleteProductPrices(id, [product_id])
}
const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
let deletedPriceIds: string[] = []
if (isMedusaV2FlagEnabled) {
const deletePriceListProductsWorkflow = removePriceListProductPrices(
req.scope
)
const input = {
product_ids: [product_id],
price_list_id: id,
} as WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO
const { result } = await deletePriceListProductsWorkflow.run({
input,
context: {
manager,
},
})
deletedPriceIds = result
} else {
const [deletedIds] = await manager.transaction(
async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deleteProductPrices(id, [product_id])
}
)
deletedPriceIds = deletedIds
}
return res.json({
ids: deletedPriceIds,
object: "money-amount",
@@ -1,8 +1,11 @@
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { removePriceListProductPrices } from "@medusajs/workflows"
import { ArrayNotEmpty, IsString } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import PriceListService from "../../../../services/price-list"
import { validator } from "../../../../utils/validator"
import { WorkflowTypes } from "@medusajs/types"
/**
* @oas [delete] /admin/price-lists/{id}/products/prices/batch
@@ -79,15 +82,44 @@ export default async (req: Request, res: Response) => {
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
const [deletedPriceIds] = await manager.transaction(
async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deleteProductPrices(id, validated.product_ids)
}
const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
let deletedPriceIds: string[] = []
if (isMedusaV2FlagEnabled) {
const deletePriceListProductsWorkflow = removePriceListProductPrices(
req.scope
)
const input = {
product_ids: validated.product_ids,
price_list_id: id,
} as WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO
const { result } = await deletePriceListProductsWorkflow.run({
input,
context: {
manager,
},
})
deletedPriceIds = result
} else {
const [deletedIds] = await manager.transaction(
async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deleteProductPrices(id, validated.product_ids)
}
)
deletedPriceIds = deletedIds
}
return res.json({
ids: deletedPriceIds,
object: "money-amount",
@@ -1,5 +1,8 @@
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { removePriceListVariantPrices } from "@medusajs/workflows"
import { EntityManager } from "typeorm"
import PriceListService from "../../../../services/price-list"
import { WorkflowTypes } from "@medusajs/types"
/**
* @oas [delete] /admin/price-lists/{id}/variants/{variant_id}/prices
@@ -59,16 +62,44 @@ export default async (req, res) => {
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
const [deletedPriceIds] = await manager.transaction(
async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deleteVariantPrices(id, [variant_id])
}
const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled(
MedusaV2Flag.key
)
let deletedPriceIds: string[] = []
if (isMedusaV2FlagEnabled) {
const deletePriceListProductsWorkflow = removePriceListVariantPrices(
req.scope
)
const input = {
variant_ids: [variant_id],
price_list_id: id,
} as WorkflowTypes.PriceListWorkflow.RemovePriceListVariantsWorkflowInputDTO
const { result } = await deletePriceListProductsWorkflow.run({
input,
context: {
manager,
},
})
deletedPriceIds = result
} else {
const [deletedIds] = await manager.transaction(
async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.deleteVariantPrices(id, [variant_id])
}
)
deletedPriceIds = deletedIds
}
return res.json({
ids: deletedPriceIds,
object: "money-amount",
@@ -1,7 +1,9 @@
import { MedusaContainer } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
import { PriceList } from "../../../.."
import PriceListService from "../../../../services/price-list"
import { getPriceListPricingModule } from "./modules-queries"
/**
* @oas [get] /admin/price-lists/{id}
@@ -58,13 +60,22 @@ import PriceListService from "../../../../services/price-list"
export default async (req, res) => {
const { id } = req.params
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const priceList = await priceListService.retrieve(id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
let priceList
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
priceList = await getPriceListPricingModule(id, {
container: req.scope as MedusaContainer,
})
} else {
priceList = await priceListService.retrieve(id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
}
res.status(200).json({ price_list: priceList })
}
@@ -9,6 +9,7 @@ import middlewares, {
import {
defaultAdminProductFields,
defaultAdminProductRelations,
defaultAdminProductRemoteQueryObject,
} from "../products"
import { FlagRouter } from "@medusajs/utils"
@@ -86,6 +87,50 @@ export default (app, featureFlagRouter: FlagRouter) => {
return app
}
export const defaultAdminPriceListRemoteQueryObject = {
fields: [
"created_at",
"deleted_at",
"description",
"ends_at",
"id",
"title",
"starts_at",
"status",
"type",
"updated_at",
],
price_list_rules: {
price_list_rule_values: {
fields: ["value"],
},
rule_type: {
fields: ["rule_attribute"],
},
},
price_set_money_amounts: {
money_amount: {
fields: [
"id",
"currency_code",
"amount",
"min_quantity",
"max_quantity",
"created_at",
"deleted_at",
"updated_at",
],
},
price_set: {
variant_link: {
variant: {
fields: defaultAdminProductRemoteQueryObject.variants.fields,
},
},
},
},
}
export const defaultAdminPriceListFields = [
"id",
"name",
@@ -1,7 +1,3 @@
import {
DateComparisonOperator,
extendedFindParamsMixin,
} from "../../../../types/common"
import {
IsArray,
IsBoolean,
@@ -10,14 +6,20 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { MedusaError, isDefined } from "medusa-core-utils"
import { isDefined } from "medusa-core-utils"
import {
DateComparisonOperator,
extendedFindParamsMixin,
} from "../../../../types/common"
import { FilterableProductProps } from "../../../../types/product"
import PriceListService from "../../../../services/price-list"
import { ProductStatus } from "../../../../models"
import { Request } from "express"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { Type } from "class-transformer"
import { Request } from "express"
import { pickBy } from "lodash"
import { ProductStatus } from "../../../../models"
import PriceListService from "../../../../services/price-list"
import { FilterableProductProps } from "../../../../types/product"
import { listAndCountProductWithIsolatedProductModule } from "../products/list-products"
/**
* @oas [get] /admin/price-lists/{id}/products
@@ -181,6 +183,9 @@ import { pickBy } from "lodash"
export default async (req: Request, res) => {
const { id } = req.params
const { offset, limit } = req.validatedQuery
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
let products
let count
const priceListService: PriceListService =
req.scope.resolve("priceListService")
@@ -190,11 +195,19 @@ export default async (req: Request, res) => {
price_list_id: [id],
}
const [products, count] = await priceListService.listProducts(
id,
pickBy(filterableFields, (val) => isDefined(val)),
req.listConfig
)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
;[products, count] = await listAndCountProductWithIsolatedProductModule(
req,
filterableFields,
req.listConfig
)
} else {
;[products, count] = await priceListService.listProducts(
id,
pickBy(filterableFields, (val) => isDefined(val)),
req.listConfig
)
}
res.json({
products,
@@ -1,9 +1,11 @@
import { IsNumber, IsOptional, IsString } from "class-validator"
import { FilterablePriceListProps } from "../../../../types/price-list"
import PriceListService from "../../../../services/price-list"
import { Request } from "express"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { Type } from "class-transformer"
import { IsNumber, IsOptional, IsString } from "class-validator"
import { Request } from "express"
import PriceListService from "../../../../services/price-list"
import { FilterablePriceListProps } from "../../../../types/price-list"
import { MedusaContainer } from "@medusajs/types"
import { listAndCountPriceListPricingModule } from "./modules-queries"
/**
* @oas [get] /admin/price-lists
@@ -161,18 +163,30 @@ import { Type } from "class-transformer"
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res) => {
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const validated = req.validatedQuery
let priceLists
let count
const priceListService: PriceListService =
req.scope.resolve("priceListService")
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
;[priceLists, count] = await listAndCountPriceListPricingModule({
filters: req.filterableFields,
listConfig: req.listConfig,
container: req.scope as MedusaContainer,
})
} else {
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const [price_lists, count] = await priceListService.listAndCount(
req.filterableFields,
req.listConfig
)
;[priceLists, count] = await priceListService.listAndCount(
req.filterableFields,
req.listConfig
)
}
res.json({
price_lists,
price_lists: priceLists,
count,
offset: validated.offset,
limit: validated.limit,
@@ -0,0 +1,29 @@
import { MedusaContainer } from "@medusajs/types"
import { PriceList } from "../../../../../models"
import { MedusaError } from "medusa-core-utils"
import { listAndCountPriceListPricingModule } from "./list-and-count-price-lists"
export async function getPriceListPricingModule(
id: string,
{
container,
}: {
container: MedusaContainer
}
): Promise<PriceList> {
const [priceLists, count] = await listAndCountPriceListPricingModule({
filters: {
id: [id],
},
container,
})
if (count === 0) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Price list with id: ${id} was not found`
)
}
return priceLists[0]
}
@@ -0,0 +1,2 @@
export * from "./get-price-list"
export * from "./list-and-count-price-lists"
@@ -0,0 +1,104 @@
import { FilterablePriceListProps, MedusaContainer } from "@medusajs/types"
import { FindConfig } from "../../../../../types/common"
import { CustomerGroup, MoneyAmount, PriceList } from "../../../../../models"
import { CustomerGroupService } from "../../../../../services"
import { defaultAdminPriceListRemoteQueryObject } from "../index"
export async function listAndCountPriceListPricingModule({
filters,
listConfig = { skip: 0 },
container,
}: {
container: MedusaContainer
filters?: FilterablePriceListProps
listConfig?: FindConfig<PriceList>
}): Promise<[PriceList[], number]> {
const remoteQuery = container.resolve("remoteQuery")
const customerGroupService: CustomerGroupService = container.resolve(
"customerGroupService"
)
const query = {
price_list: {
__args: { filters, ...listConfig },
...defaultAdminPriceListRemoteQueryObject,
},
}
const {
rows: priceLists,
metadata: { count },
} = await remoteQuery(query)
if (!count) {
return [[], 0]
}
const customerGroupIds: string[] = priceLists
.map((priceList) =>
priceList.price_list_rules
.filter((rule) => rule.rule_type.rule_attribute === "customer_group_id")
.map((rule) =>
rule.price_list_rule_values.map((rule_value) => rule_value.value)
)
)
.flat(2)
const priceListCustomerGroups = await customerGroupService.list(
{ id: customerGroupIds },
{}
)
const customerGroupIdCustomerGroupMap = new Map(
priceListCustomerGroups.map((customerGroup) => [
customerGroup.id,
customerGroup,
])
)
for (const priceList of priceLists) {
const priceSetMoneyAmounts = priceList.price_set_money_amounts || []
const priceListRulesData = priceList.price_list_rules || []
delete priceList.price_set_money_amounts
delete priceList.price_list_rules
priceList.prices = priceSetMoneyAmounts.map((priceSetMoneyAmount) => {
const productVariant = priceSetMoneyAmount.price_set.variant_link.variant
return {
...(priceSetMoneyAmount.money_amount as MoneyAmount),
price_list_id: priceList.id,
variant_id: productVariant?.id ?? null,
variant: productVariant ?? null,
region_id: null,
}
})
priceList.name = priceList.title
delete priceList.title
const customerGroupPriceListRule = priceListRulesData.find(
(plr) => plr.rule_type.rule_attribute === "customer_group_id"
)
if (
customerGroupPriceListRule &&
customerGroupPriceListRule?.price_list_rule_values
) {
priceList.customer_groups =
customerGroupPriceListRule?.price_list_rule_values
.map((customerGroupRule) =>
customerGroupIdCustomerGroupMap.get(customerGroupRule.value)
)
.filter(
(
customerGroup: CustomerGroup | undefined
): customerGroup is CustomerGroup => !!customerGroup
)
} else {
priceList.customer_groups = []
}
}
return [priceLists, count]
}
@@ -1,4 +1,5 @@
import { PriceListStatus, PriceListType } from "@medusajs/utils"
import { MedusaContainer, PricingTypes, WorkflowTypes } from "@medusajs/types"
import { MedusaV2Flag, PriceListStatus, PriceListType } from "@medusajs/utils"
import {
IsArray,
IsBoolean,
@@ -8,15 +9,17 @@ import {
ValidateNested,
} from "class-validator"
import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "."
import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list"
import { updatePriceLists } from "@medusajs/workflows"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import { PriceList } from "../../../.."
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import PriceListService from "../../../../services/price-list"
import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { validator } from "../../../../utils/validator"
import { getPriceListPricingModule } from "./modules-queries"
/**
* @oas [post] /admin/price-lists/{id}
@@ -83,26 +86,59 @@ import { validator } from "../../../../utils/validator"
*/
export default async (req, res) => {
const { id } = req.params
let priceList
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const manager: EntityManager = req.scope.resolve("manager")
const priceListService: PriceListService =
req.scope.resolve("priceListService")
const validated = await validator(
AdminPostPriceListsPriceListPriceListReq,
req.body
)
const priceListService: PriceListService =
req.scope.resolve("priceListService")
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const updateVariantsWorkflow = updatePriceLists(req.scope)
const rules: PricingTypes.CreatePriceListRules = {}
const customerGroups = validated.customer_groups || []
delete validated.customer_groups
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.update(id, validated)
})
if (customerGroups.length) {
rules["customer_group_id"] = customerGroups.map((group) => group.id)
}
const priceList = await priceListService.retrieve(id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
const input = {
price_lists: [
{
id,
...validated,
rules,
},
],
} as WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowInputDTO
await updateVariantsWorkflow.run({
input,
context: {
manager,
},
})
priceList = await getPriceListPricingModule(id, {
container: req.scope as MedusaContainer,
})
} else {
await manager.transaction(async (transactionManager) => {
return await priceListService
.withTransaction(transactionManager)
.update(id, validated)
})
priceList = await priceListService.retrieve(id, {
select: defaultAdminPriceListFields as (keyof PriceList)[],
relations: defaultAdminPriceListRelations,
})
}
res.json({ price_list: priceList })
}
@@ -1,7 +1,6 @@
import {
CreateProductVariantInput,
ProductVariantPricesCreateReq,
} from "../../../../types/product-variant"
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { CreateProductVariants } from "@medusajs/workflows"
import {
IsArray,
IsBoolean,
@@ -11,19 +10,22 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import {
PricingService,
ProductService,
ProductVariantInventoryService,
ProductVariantService,
} from "../../../../services"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import { EntityManager } from "typeorm"
import { IInventoryService } from "@medusajs/types"
import {
CreateProductVariantInput,
ProductVariantPricesCreateReq,
} from "../../../../types/product-variant"
import { Type } from "class-transformer"
import { createVariantsTransaction } from "./transaction/create-product-variant"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { getProductWithIsolatedProductModule } from "./get-product"
import { createVariantsTransaction } from "./transaction/create-product-variant"
/**
* @oas [post] /admin/products/{id}/variants
@@ -122,6 +124,8 @@ export default async (req, res) => {
req.body
)
const manager: EntityManager = req.scope.resolve("manager")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const inventoryService: IInventoryService | undefined =
req.scope.resolve("inventoryService")
const productVariantInventoryService: ProductVariantInventoryService =
@@ -129,29 +133,56 @@ export default async (req, res) => {
const productVariantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
await createVariantsTransaction(
{
manager: transactionManager,
inventoryService,
productVariantInventoryService,
productVariantService,
},
id,
[validated as CreateProductVariantInput]
)
})
const productService: ProductService = req.scope.resolve("productService")
const pricingService: PricingService = req.scope.resolve("pricingService")
let rawProduct
const rawProduct = await productService.retrieve(id, {
select: defaultAdminProductFields,
relations: defaultAdminProductRelations,
})
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const createVariantsWorkflow = CreateProductVariants.createProductVariants(
req.scope
)
const input = {
productVariants: [
{
product_id: id,
...validated,
},
] as WorkflowTypes.ProductWorkflow.CreateProductVariantsInputDTO[],
}
await createVariantsWorkflow.run({
input,
context: {
manager,
},
})
rawProduct = await getProductWithIsolatedProductModule(
req,
id,
req.retrieveConfig
)
} else {
await manager.transaction(async (transactionManager) => {
await createVariantsTransaction(
{
manager: transactionManager,
inventoryService,
productVariantInventoryService,
productVariantService,
},
id,
[validated as CreateProductVariantInput]
)
})
const productService: ProductService = req.scope.resolve("productService")
rawProduct = await productService.retrieve(id, {
select: defaultAdminProductFields,
relations: defaultAdminProductRelations,
})
}
const [product] = await pricingService.setAdminProductPricing([rawProduct])
@@ -118,7 +118,11 @@ export default async (req, res) => {
res.json({ product })
}
async function getProductWithIsolatedProductModule(req, id, retrieveConfig) {
export async function getProductWithIsolatedProductModule(
req,
id,
retrieveConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
@@ -7,7 +7,11 @@ import {
SalesChannelService,
} from "../../../../services"
import { IInventoryService } from "@medusajs/types"
import {
IInventoryService,
IPricingModuleService,
IProductModuleService,
} from "@medusajs/types"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { Type } from "class-transformer"
import { Product } from "../../../../models"
@@ -301,7 +305,53 @@ export default async (req, res) => {
})
}
async function listAndCountProductWithIsolatedProductModule(
async function getVariantsFromPriceList(req, priceListId) {
const remoteQuery = req.scope.resolve("remoteQuery")
const pricingModuleService: IPricingModuleService = req.scope.resolve(
"pricingModuleService"
)
const productModuleService: IProductModuleService = req.scope.resolve(
"productModuleService"
)
const [priceList] = await pricingModuleService.listPriceLists(
{ id: [priceListId] },
{
relations: [
"price_set_money_amounts",
"price_set_money_amounts.price_set",
],
select: ["price_set_money_amounts.price_set.id"],
}
)
const priceSetIds = priceList.price_set_money_amounts?.map(
(psma) => psma.price_set?.id
)
const query = {
product_variant_price_set: {
__args: {
price_set_id: priceSetIds,
},
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIds = variantPriceSets.map((vps) => vps.variant_id)
return await productModuleService.listVariants(
{
id: variantIds,
},
{
select: ["product_id"],
}
)
}
export async function listAndCountProductWithIsolatedProductModule(
req,
filterableFields,
listConfig
@@ -309,6 +359,7 @@ async function listAndCountProductWithIsolatedProductModule(
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const productIdsFilter: Set<string> = new Set()
const variantIdsFilter: Set<string> = new Set()
@@ -352,22 +403,28 @@ async function listAndCountProductWithIsolatedProductModule(
delete filterableFields.price_list_id
if (priceListId) {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = req.scope.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const variants = await getVariantsFromPriceList(req, priceListId)
variants.forEach((pv) => variantIdsFilter.add(pv.id))
} else {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = req.scope.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
}
}
const discountConditionId = filterableFields.discount_condition_id
+7 -5
View File
@@ -5,7 +5,7 @@ import getMigrations, {
runIsolatedModulesMigration,
} from "./utils/get-migrations"
import { MedusaV2Flag, createMedusaContainer } from "@medusajs/utils"
import { createMedusaContainer } from "@medusajs/utils"
import configModuleLoader from "../loaders/config"
import databaseLoader from "../loaders/database"
import featureFlagLoader from "../loaders/feature-flags"
@@ -38,6 +38,11 @@ const getDataSource = async (directory) => {
const runLinkMigrations = async (directory) => {
const configModule = configModuleLoader(directory)
const container = createMedusaContainer()
const featureFlagRouter = featureFlagLoader(configModule)
container.register({
featureFlagRouter: asValue(featureFlagRouter),
})
await pgConnectionLoader({ configModule, container })
@@ -63,16 +68,13 @@ const main = async function ({ directory }) {
const configModule = configModuleLoader(directory)
const dataSource = await getDataSource(directory)
const featureFlagRouter = featureFlagLoader(configModule)
if (args[0] === "run") {
await dataSource.runMigrations()
await dataSource.destroy()
await runIsolatedModulesMigration(configModule)
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
await runLinkMigrations(directory)
}
await runLinkMigrations(directory)
process.exit()
Logger.info("Migrations completed.")
@@ -0,0 +1,20 @@
import { MedusaV2Flag } from "@medusajs/utils"
import { MigrationInterface, QueryRunner } from "typeorm"
export const featureFlag = MedusaV2Flag.key
export class DropNonNullConstraintPriceList1699371074198
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE IF EXISTS price_list ALTER COLUMN name DROP NOT NULL;
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE IF EXISTS price_list ALTER COLUMN name SET NOT NULL;
`)
}
}
+1 -1
View File
@@ -69,7 +69,7 @@ export class MoneyAmount extends SoftDeletableEntity {
@Index("idx_money_amount_region_id")
@Column({ nullable: true })
region_id: string
region_id: string | null
@ManyToOne(() => Region)
@JoinColumn({ name: "region_id" })
@@ -11,17 +11,28 @@ export const createDefaultRuleTypes = async (container: AwilixContainer) => {
"pricingModuleService"
)
const existing = await pricingModuleService.listRuleTypes(
{ rule_attribute: ["region_id"] },
{ take: 1 }
{ rule_attribute: ["region_id", "customer_group_id"] },
{ take: 2 }
)
if (existing.length) {
if (existing.length === 2) {
return
}
await pricingModuleService.createRuleTypes([
{ name: "region_id", rule_attribute: "region_id" },
])
if (existing.length === 0) {
await pricingModuleService.createRuleTypes([
{ name: "region_id", rule_attribute: "region_id" },
{ name: "customer_group_id", rule_attribute: "customer_group_id" },
])
} else if (existing[0].rule_attribute === "region_id") {
await pricingModuleService.createRuleTypes([
{ name: "customer_group_id", rule_attribute: "customer_group_id" },
])
} else {
await pricingModuleService.createRuleTypes([
{ name: "region_id", rule_attribute: "region_id" },
])
}
}
const migrate = async function ({ directory }) {
@@ -38,7 +49,9 @@ const migrate = async function ({ directory }) {
migrate({ directory: process.cwd() })
.then(() => {
console.log("Created default rule types")
process.exit()
})
.catch(() => {
console.log("Failed to create rule types")
process.exit(1)
})
@@ -0,0 +1,146 @@
import { IPricingModuleService, PricingTypes } from "@medusajs/types"
import { AwilixContainer } from "awilix"
import dotenv from "dotenv"
import express from "express"
import loaders from "../loaders"
import Logger from "../loaders/logger"
import { PriceListService } from "../services"
dotenv.config()
const BATCH_SIZE = 1000
export const migratePriceLists = async (container: AwilixContainer) => {
const pricingModuleService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
const priceListCoreService: PriceListService =
container.resolve("priceListService")
const remoteQuery = container.resolve("remoteQuery")
const existingRuleTypes = await pricingModuleService.listRuleTypes(
{ rule_attribute: ["customer_group_id"] },
{ take: 2 }
)
if (existingRuleTypes.length === 0) {
Logger.info(
`Run default rules migration before running this migration - node node_modules/@medusajs/medusa/dist/scripts/create-default-rule-types.js`
)
return
}
let offset = 0
let arePriceListsAvailable = true
while (arePriceListsAvailable) {
const priceLists = await priceListCoreService.list(
{},
{
take: BATCH_SIZE,
skip: offset,
relations: ["customer_groups"],
}
)
if (priceLists.length === 0) {
break
}
offset += BATCH_SIZE
await pricingModuleService.update(
priceLists.map((priceList) => {
const updateData: PricingTypes.UpdatePriceListDTO = {
id: priceList.id,
title: priceList.name,
}
if (priceList?.customer_groups?.length) {
updateData.rules = {
customer_group_id: priceList.customer_groups.map((cg) => cg.id),
}
}
return updateData
})
)
for (const priceList of priceLists) {
let productsOffset = 0
let areVariantsAvailable = true
while (areVariantsAvailable) {
const [priceListVariants, variantsCount] =
await priceListCoreService.listVariants(
priceList.id,
{},
{
skip: productsOffset,
take: BATCH_SIZE,
}
)
if (variantsCount === 0) {
break
}
productsOffset += BATCH_SIZE
const query = {
product_variant_price_set: {
__args: {
variant_id: priceListVariants.map((plv) => plv.id),
},
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantPriceSetMap = new Map<string, string>(
variantPriceSets.map((mps) => [mps.variant_id, mps.price_set_id])
)
const variantPrices = priceListVariants
.map((plv) => plv.prices || [])
.flat()
await pricingModuleService.addPriceListPrices([
{
priceListId: priceList.id,
prices: variantPrices.map((vp) => {
return {
id: vp.id,
price_set_id: variantPriceSetMap.get(vp.variant_id)!,
currency_code: vp.currency_code,
amount: vp.amount,
}
}),
},
])
}
}
}
}
const migrate = async function ({ directory }) {
const app = express()
const { container } = await loaders({
directory,
expressApp: app,
isTest: false,
})
return await migratePriceLists(container)
}
migrate({ directory: process.cwd() })
.then(() => {
console.log("Migrated price lists")
})
.catch(() => {
console.log("Failed to migrate price lists")
})
@@ -45,7 +45,7 @@ const migrateProductVariant = async (
rules: [{ rule_attribute: "region_id" }],
prices: variant.prices.map((price) => ({
rules: {
region_id: price.region_id,
...(price.region_id ? { region_id: price.region_id } : {}),
},
currency_code: price.currency_code,
min_quantity: price.min_quantity,
+54 -23
View File
@@ -4,15 +4,18 @@ import {
PriceSetMoneyAmountDTO,
RemoteQueryFunction,
} from "@medusajs/types"
import {
CustomerService,
ProductVariantService,
RegionService,
TaxProviderService,
} from "."
import {
FlagRouter,
MedusaV2Flag,
promiseAll,
removeNullish,
} from "@medusajs/utils"
import { ProductVariantService, RegionService, TaxProviderService } from "."
import {
IPriceSelectionStrategy,
PriceSelectionContext,
@@ -33,11 +36,11 @@ import {
TaxedPricing,
} from "../types/pricing"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import { MedusaError } from "medusa-core-utils"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import { TaxServiceRate } from "../types/tax-service"
import { TransactionBaseService } from "../interfaces"
import { calculatePriceTaxAmount } from "../utils"
type InjectedDependencies = {
@@ -45,6 +48,7 @@ type InjectedDependencies = {
productVariantService: ProductVariantService
taxProviderService: TaxProviderService
regionService: RegionService
customerService: CustomerService
priceSelectionStrategy: IPriceSelectionStrategy
featureFlagRouter: FlagRouter
remoteQuery: RemoteQueryFunction
@@ -57,6 +61,7 @@ type InjectedDependencies = {
class PricingService extends TransactionBaseService {
protected readonly regionService: RegionService
protected readonly taxProviderService: TaxProviderService
protected readonly customerService_: CustomerService
protected readonly priceSelectionStrategy: IPriceSelectionStrategy
protected readonly productVariantService: ProductVariantService
protected readonly featureFlagRouter: FlagRouter
@@ -64,6 +69,7 @@ class PricingService extends TransactionBaseService {
protected get pricingModuleService(): IPricingModuleService {
return this.__container__.pricingModuleService
}
protected get remoteQuery(): RemoteQueryFunction {
return this.__container__.remoteQuery
}
@@ -74,6 +80,7 @@ class PricingService extends TransactionBaseService {
regionService,
priceSelectionStrategy,
featureFlagRouter,
customerService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
@@ -82,6 +89,7 @@ class PricingService extends TransactionBaseService {
this.taxProviderService = taxProviderService
this.priceSelectionStrategy = priceSelectionStrategy
this.productVariantService = productVariantService
this.customerService_ = customerService
this.featureFlagRouter = featureFlagRouter
}
@@ -220,9 +228,20 @@ class PricingService extends TransactionBaseService {
(variantPriceSet) => variantPriceSet.price_set_id
)
const queryContext: PriceSelectionContext = removeNullish(
context.price_selection
)
const queryContext: PriceSelectionContext & {
customer_group_id?: string[]
} = removeNullish(context.price_selection)
if (queryContext.customer_id) {
const { groups } = await this.customerService_.retrieve(
queryContext.customer_id,
{ relations: ["groups"] }
)
if (groups?.length) {
queryContext.customer_group_id = groups.map((group) => group.id)
}
}
let calculatedPrices: CalculatedPriceSet[] = []
@@ -259,25 +278,37 @@ class PricingService extends TransactionBaseService {
}
if (priceSetId) {
const calculatedPrice: CalculatedPriceSet | undefined =
const calculatedPrices: CalculatedPriceSet | undefined =
calculatedPriceMap.get(priceSetId)
if (calculatedPrice) {
pricingResult.prices = [
{
id: calculatedPrice.calculated_price?.money_amount_id,
currency_code: calculatedPrice.currency_code,
amount: calculatedPrice.calculated_amount,
min_quantity: calculatedPrice.calculated_price?.min_quantity,
max_quantity: calculatedPrice.calculated_price?.max_quantity,
price_list_id: calculatedPrice.calculated_price?.price_list_id,
},
] as MoneyAmount[]
if (calculatedPrices) {
pricingResult.prices.push({
id: calculatedPrices?.original_price?.money_amount_id,
currency_code: calculatedPrices.currency_code,
amount: calculatedPrices.original_amount,
min_quantity: calculatedPrices.original_price?.min_quantity,
max_quantity: calculatedPrices.original_price?.max_quantity,
price_list_id: calculatedPrices.original_price?.price_list_id,
} as MoneyAmount)
pricingResult.original_price = calculatedPrice?.original_amount
pricingResult.calculated_price = calculatedPrice?.calculated_amount
if (
calculatedPrices.calculated_price?.money_amount_id !==
calculatedPrices.original_price?.money_amount_id
) {
pricingResult.prices.push({
id: calculatedPrices.calculated_price?.money_amount_id,
currency_code: calculatedPrices.currency_code,
amount: calculatedPrices.calculated_amount,
min_quantity: calculatedPrices.calculated_price?.min_quantity,
max_quantity: calculatedPrices.calculated_price?.max_quantity,
price_list_id: calculatedPrices.calculated_price?.price_list_id,
} as MoneyAmount)
}
pricingResult.original_price = calculatedPrices?.original_amount
pricingResult.calculated_price = calculatedPrices?.calculated_amount
pricingResult.calculated_price_type =
calculatedPrice?.calculated_price?.price_list_type
calculatedPrices?.calculated_price?.price_list_type
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import {
ValidateNested,
} from "class-validator"
import { PriceList } from "../models/price-list"
import { DateComparisonOperator, FindConfig } from "./common"
import { DateComparisonOperator } from "./common"
import { XorConstraint } from "./validators/xor"
/**