feat(core-flows,types,pricing,medusa): Products API can create prices with rules (#7796)

* chore: Products API can create prices with rules

* chore: fix tests

* chore: cleanup

* chore: address comments
This commit is contained in:
Riqwan Thamir
2024-06-21 15:30:36 +02:00
committed by GitHub
parent ed104d5aac
commit 8ac74c1357
11 changed files with 131 additions and 741 deletions
@@ -1227,6 +1227,64 @@ medusaIntegrationTestRunner({
) )
}) })
it("creates a product variant with price rules", async () => {
await api.post(
`/admin/pricing/rule-types`,
{
name: "Region",
rule_attribute: "region_id",
default_priority: 1,
},
adminHeaders
)
const response = await api.post(
"/admin/products",
{
title: "Test create",
variants: [
{
title: "Price with rules",
prices: [
{
currency_code: "usd",
amount: 100,
rules: { region_id: "eur" },
},
],
},
],
},
adminHeaders
)
const priceIdSelector = /^price_*/
expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: expect.stringMatching(/^prod_*/),
variants: expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(/^variant_*/),
title: "Price with rules",
prices: expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(priceIdSelector),
currency_code: "usd",
amount: 100,
created_at: expect.any(String),
updated_at: expect.any(String),
variant_id: expect.stringMatching(/^variant_*/),
rules: { region_id: "eur" },
}),
]),
}),
]),
})
)
})
it("creates a product that is not discountable", async () => { it("creates a product that is not discountable", async () => {
const payload = { const payload = {
title: "Test", title: "Test",
@@ -96,11 +96,20 @@ medusaIntegrationTestRunner({
describe("updates a variant's default prices (ignores prices associated with a Price List)", () => { describe("updates a variant's default prices (ignores prices associated with a Price List)", () => {
it("successfully updates a variant's default prices by changing an existing price (currency_code)", async () => { it("successfully updates a variant's default prices by changing an existing price (currency_code)", async () => {
await api.post(
`/admin/pricing/rule-types`,
{ name: "Region", rule_attribute: "region_id", default_priority: 1 },
adminHeaders
)
const data = { const data = {
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
amount: 1500, amount: 1500,
rules: {
region_id: "na",
},
}, },
], ],
} }
@@ -115,6 +124,7 @@ medusaIntegrationTestRunner({
baseProduct.variants[0].prices.find((p) => p.currency_code === "usd") baseProduct.variants[0].prices.find((p) => p.currency_code === "usd")
.amount .amount
).toEqual(100) ).toEqual(100)
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data).toEqual({ expect(response.data).toEqual({
product: expect.objectContaining({ product: expect.objectContaining({
@@ -126,6 +136,7 @@ medusaIntegrationTestRunner({
expect.objectContaining({ expect.objectContaining({
amount: 1500, amount: 1500,
currency_code: "usd", currency_code: "usd",
rules: { region_id: "na" },
}), }),
]), ]),
}), }),
@@ -43,9 +43,10 @@ export const updatePriceSetsStep = createStep(
return new StepResponse([], null) return new StepResponse([], null)
} }
const { selects, relations } = getSelectsAndRelationsFromObjectArray([ const { selects, relations } = getSelectsAndRelationsFromObjectArray(
data.update, [data.update],
]) { objectFields: ["rules"] }
)
const dataBeforeUpdate = await pricingModule.listPriceSets(data.selector, { const dataBeforeUpdate = await pricingModule.listPriceSets(data.selector, {
select: selects, select: selects,
@@ -5,7 +5,6 @@ import { Context } from "../shared-context"
import { import {
AddPriceListPricesDTO, AddPriceListPricesDTO,
AddPricesDTO, AddPricesDTO,
AddRulesDTO,
CalculatedPriceSet, CalculatedPriceSet,
CreatePriceListDTO, CreatePriceListDTO,
CreatePriceRuleDTO, CreatePriceRuleDTO,
@@ -25,7 +24,6 @@ import {
PricingContext, PricingContext,
PricingFilters, PricingFilters,
RemovePriceListRulesDTO, RemovePriceListRulesDTO,
RemovePriceSetRulesDTO,
RuleTypeDTO, RuleTypeDTO,
SetPriceListRulesDTO, SetPriceListRulesDTO,
UpdatePriceListDTO, UpdatePriceListDTO,
@@ -481,26 +479,6 @@ export interface IPricingModuleService extends IModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<PriceSetDTO[]> ): Promise<PriceSetDTO[]>
/**
* This method remove rules from a price set.
*
* @param {RemovePriceSetRulesDTO[]} data - The rules to remove per price set.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<void>} Resolves when rules are successfully removed.
*
* @example
* await pricingModuleService.removeRules([
* {
* id: "pset_123",
* rules: ["region_id"],
* },
* ])
*/
removeRules(
data: RemovePriceSetRulesDTO[],
sharedContext?: Context
): Promise<void>
/** /**
* This method deletes price sets by their IDs. * This method deletes price sets by their IDs.
* *
@@ -615,54 +593,6 @@ export interface IPricingModuleService extends IModuleService {
sharedContext?: Context sharedContext?: Context
): Promise<PriceSetDTO[]> ): Promise<PriceSetDTO[]>
/**
* This method adds rules to a price set.
*
* @param {AddRulesDTO} data - The data defining the price set to add the rules to, along with the rules to add.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<PriceSetDTO>} The price set that the rules were added to.
*
* @example
* const priceSet = await pricingModuleService.addRules({
* priceSetId: "pset_123",
* rules: [
* {
* attribute: "region_id",
* },
* ],
* })
*/
addRules(data: AddRulesDTO, sharedContext?: Context): Promise<PriceSetDTO>
/**
* This method adds rules to multiple price sets.
*
* @param {AddRulesDTO[]} data - The data defining the rules to add per price set.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<PriceSetDTO[]>} The list of the price sets that the rules were added to.
*
* @example
* const priceSets = await pricingModuleService.addRules([
* {
* priceSetId: "pset_123",
* rules: [
* {
* attribute: "region_id",
* },
* ],
* },
* {
* priceSetId: "pset_321",
* rules: [
* {
* attribute: "customer_group_id",
* },
* ],
* },
* ])
*/
addRules(data: AddRulesDTO[], sharedContext?: Context): Promise<PriceSetDTO[]>
/** /**
* This method is used to retrieve a rule type by its ID and and optionally based on the provided configurations. * This method is used to retrieve a rule type by its ID and and optionally based on the provided configurations.
* *
@@ -1,20 +1,20 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import { import {
deleteProductVariantsWorkflow, deleteProductVariantsWorkflow,
updateProductVariantsWorkflow, updateProductVariantsWorkflow,
} from "@medusajs/core-flows" } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import { HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../../../utils/refetch-entity"
import { import {
remapKeysForProduct, remapKeysForProduct,
remapKeysForVariant, remapKeysForVariant,
remapProductResponse, remapProductResponse,
remapVariantResponse, remapVariantResponse,
} from "../../../helpers" } from "../../../helpers"
import { HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../../../utils/refetch-entity"
export const GET = async ( export const GET = async (
req: AuthenticatedMedusaRequest, req: AuthenticatedMedusaRequest,
@@ -54,6 +54,7 @@ export const POST = async (
req.scope, req.scope,
remapKeysForProduct(req.remoteQueryConfig.fields ?? []) remapKeysForProduct(req.remoteQueryConfig.fields ?? [])
) )
res.status(200).json({ product: remapProductResponse(product) }) res.status(200).json({ product: remapProductResponse(product) })
} }
@@ -3,6 +3,7 @@ import {
BatchMethodResponse, BatchMethodResponse,
HttpTypes, HttpTypes,
MedusaContainer, MedusaContainer,
PriceDTO,
ProductDTO, ProductDTO,
ProductVariantDTO, ProductVariantDTO,
} from "@medusajs/types" } from "@medusajs/types"
@@ -25,6 +26,7 @@ export const remapKeysForProduct = (selectFields: string[]) => {
const productFields = selectFields.filter( const productFields = selectFields.filter(
(fieldName: string) => !isPricing(fieldName) (fieldName: string) => !isPricing(fieldName)
) )
const pricingFields = selectFields const pricingFields = selectFields
.filter((fieldName: string) => isPricing(fieldName)) .filter((fieldName: string) => isPricing(fieldName))
.map((fieldName: string) => .map((fieldName: string) =>
@@ -38,6 +40,7 @@ export const remapKeysForVariant = (selectFields: string[]) => {
const variantFields = selectFields.filter( const variantFields = selectFields.filter(
(fieldName: string) => !isPricing(fieldName) (fieldName: string) => !isPricing(fieldName)
) )
const pricingFields = selectFields const pricingFields = selectFields
.filter((fieldName: string) => isPricing(fieldName)) .filter((fieldName: string) => isPricing(fieldName))
.map((fieldName: string) => .map((fieldName: string) =>
@@ -75,14 +78,30 @@ export const remapVariantResponse = (
variant_id: variant.id, variant_id: variant.id,
created_at: price.created_at, created_at: price.created_at,
updated_at: price.updated_at, updated_at: price.updated_at,
rules: buildRules(price),
})), })),
} }
delete (resp as any).price_set delete (resp as any).price_set
// TODO: Remove any once all typings are cleaned up // TODO: Remove any once all typings are cleaned up
return resp as any return resp as any
} }
export const buildRules = (price: PriceDTO) => {
const rules: Record<string, string> = {}
for (const priceRule of price.price_rules || []) {
const ruleAttribute = priceRule.rule_type?.rule_attribute
if (ruleAttribute) {
rules[ruleAttribute] = priceRule.value
}
}
return rules
}
export const refetchVariant = async ( export const refetchVariant = async (
variantId: string, variantId: string,
scope: MedusaContainer, scope: MedusaContainer,
@@ -22,6 +22,8 @@ export const defaultAdminProductsVariantFields = [
"upc", "upc",
"barcode", "barcode",
"*prices", "*prices",
"prices.price_rules.value",
"prices.price_rules.rule_type.rule_attribute",
"*options", "*options",
] ]
@@ -82,6 +84,8 @@ export const defaultAdminProductFields = [
"*images", "*images",
"*variants", "*variants",
"*variants.prices", "*variants.prices",
"variants.prices.price_rules.value",
"variants.prices.price_rules.rule_type.rule_attribute",
"*variants.options", "*variants.options",
"*sales_channels", "*sales_channels",
] ]
@@ -106,6 +106,7 @@ export const AdminCreateVariantPrice = z.object({
amount: z.number(), amount: z.number(),
min_quantity: z.number().nullish(), min_quantity: z.number().nullish(),
max_quantity: z.number().nullish(), max_quantity: z.number().nullish(),
rules: z.record(z.string(), z.string()).optional(),
}) })
// TODO: Add support for rules // TODO: Add support for rules
@@ -118,6 +119,7 @@ export const AdminUpdateVariantPrice = z.object({
amount: z.number().optional(), amount: z.number().optional(),
min_quantity: z.number().nullish(), min_quantity: z.number().nullish(),
max_quantity: z.number().nullish(), max_quantity: z.number().nullish(),
rules: z.record(z.string(), z.string()).optional(),
}) })
export type AdminCreateProductTypeType = z.infer<typeof AdminCreateProductType> export type AdminCreateProductTypeType = z.infer<typeof AdminCreateProductType>
@@ -1,16 +1,16 @@
import { IPricingModuleService } from "@medusajs/types" import { IPricingModuleService } from "@medusajs/types"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { createPriceLists } from "../../../__fixtures__/price-list"
import { createPriceSets } from "../../../__fixtures__/price-set"
import { import {
CommonEvents, CommonEvents,
composeMessage, composeMessage,
Modules, Modules,
PricingEvents, PricingEvents,
} from "@medusajs/utils" } from "@medusajs/utils"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { createPriceLists } from "../../../__fixtures__/price-list"
import { createPriceSets } from "../../../__fixtures__/price-set"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -745,41 +745,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
) )
}) })
it("should fail to add a price with non-existing rule-types in the price-set to a priceList", async () => {
await service.createRuleTypes([
{
name: "twitter_handle",
rule_attribute: "twitter_handle",
},
])
let error
try {
await service.addPriceListPrices([
{
price_list_id: "price-list-1",
prices: [
{
amount: 123,
currency_code: "EUR",
price_set_id: "price-set-1",
rules: {
twitter_handle: "owjuhl",
},
},
],
},
])
} catch (err) {
error = err
}
expect(error.message).toEqual(
"" +
`Invalid rule type configuration: Price set rules doesn't exist for rule_attribute "twitter_handle" in price set price-set-1`
)
})
it("should add a price with rules to a priceList successfully", async () => { it("should add a price with rules to a priceList successfully", async () => {
await service.createRuleTypes([ await service.createRuleTypes([
{ {
@@ -788,15 +753,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}, },
]) ])
const r = await service.addRules([
{
priceSetId: "price-set-1",
rules: [{ attribute: "region_id" }],
},
])
jest.clearAllMocks()
await service.addPriceListPrices([ await service.addPriceListPrices([
{ {
price_list_id: "price-list-1", price_list_id: "price-list-1",
@@ -886,14 +842,7 @@ moduleIntegrationTestRunner<IPricingModuleService>({
describe("updatePriceListPrices", () => { describe("updatePriceListPrices", () => {
it("should update a price to a priceList successfully", async () => { it("should update a price to a priceList successfully", async () => {
const [priceSet] = await service.createPriceSets([ const [priceSet] = await service.createPriceSets([{}])
{
rules: [
{ rule_attribute: "region_id" },
{ rule_attribute: "customer_group_id" },
],
},
])
await service.addPriceListPrices([ await service.addPriceListPrices([
{ {
@@ -907,7 +856,7 @@ moduleIntegrationTestRunner<IPricingModuleService>({
rules: { rules: {
region_id: "test", region_id: "test",
}, },
} as any, },
], ],
}, },
]) ])
@@ -981,52 +930,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}) })
) )
}) })
it("should fail to add a price with non-existing rule-types in the price-set to a priceList", async () => {
await service.createRuleTypes([
{ name: "twitter_handle", rule_attribute: "twitter_handle" },
{ name: "region_id", rule_attribute: "region_id" },
])
const [priceSet] = await service.createPriceSets([
{ rules: [{ rule_attribute: "region_id" }] },
])
await service.addPriceListPrices([
{
price_list_id: "price-list-1",
prices: [
{
id: "test-price-id",
amount: 123,
currency_code: "EUR",
price_set_id: priceSet.id,
rules: { region_id: "test" },
} as any,
],
},
])
const error = await service
.updatePriceListPrices([
{
price_list_id: "price-list-1",
prices: [
{
id: "test-price-id",
amount: 123,
price_set_id: priceSet.id,
rules: { twitter_handle: "owjuhl" },
},
],
},
])
.catch((e) => e)
expect(error.message).toEqual(
`Invalid rule type configuration: Price set rules doesn't exist for rule_attribute "twitter_handle" in price set ${priceSet.id}`
)
})
}) })
describe("removePrices", () => { describe("removePrices", () => {
@@ -3,6 +3,12 @@ import {
CreatePriceSetRuleTypeDTO, CreatePriceSetRuleTypeDTO,
IPricingModuleService, IPricingModuleService,
} from "@medusajs/types" } from "@medusajs/types"
import {
CommonEvents,
composeMessage,
Modules,
PricingEvents,
} from "@medusajs/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { import {
MockEventBusService, MockEventBusService,
@@ -10,12 +16,6 @@ import {
} from "medusa-test-utils" } from "medusa-test-utils"
import { PriceSetRuleType } from "../../../../src/models" import { PriceSetRuleType } from "../../../../src/models"
import { seedPriceData } from "../../../__fixtures__/seed-price-data" import { seedPriceData } from "../../../__fixtures__/seed-price-data"
import {
CommonEvents,
composeMessage,
Modules,
PricingEvents,
} from "@medusajs/utils"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -346,24 +346,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}) })
describe("create", () => { describe("create", () => {
it("should throw an error when creating a price set with rule attributes that don't exist", async () => {
let error
try {
await service.createPriceSets([
{
rules: [{ rule_attribute: "does-not-exist" }],
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
"Rule types don't exist for: does-not-exist"
)
})
it("should fail to create a price set with rule types and money amounts with rule types that don't exits", async () => { it("should fail to create a price set with rule types and money amounts with rule types that don't exits", async () => {
let error let error
@@ -386,32 +368,13 @@ moduleIntegrationTestRunner<IPricingModuleService>({
error = e error = e
} }
expect(error.message).toEqual( expect(error.message).toEqual(
"Rule types don't exist for money amounts with rule attribute: city" "Rule types don't exist for prices with rule attribute: city"
)
})
it("should create a price set with rule types", async () => {
const [priceSet] = await service.createPriceSets([
{
rules: [{ rule_attribute: "region_id" }],
},
])
expect(priceSet).toEqual(
expect.objectContaining({
rule_types: [
expect.objectContaining({
rule_attribute: "region_id",
}),
],
})
) )
}) })
it("should create a price set with rule types and money amounts", async () => { it("should create a price set with rule types and money amounts", async () => {
const [priceSet] = await service.createPriceSets([ const [priceSet] = await service.createPriceSets([
{ {
rules: [{ rule_attribute: "region_id" }],
prices: [ prices: [
{ {
amount: 100, amount: 100,
@@ -426,11 +389,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
expect(priceSet).toEqual( expect(priceSet).toEqual(
expect.objectContaining({ expect.objectContaining({
rule_types: [
expect.objectContaining({
rule_attribute: "region_id",
}),
],
prices: [ prices: [
expect.objectContaining({ expect.objectContaining({
amount: 100, amount: 100,
@@ -477,10 +435,9 @@ moduleIntegrationTestRunner<IPricingModuleService>({
) )
}) })
it("should create a price set with money amounts with and without rules", async () => { it("should create a price set with prices", async () => {
const [priceSet] = await service.createPriceSets([ const [priceSet] = await service.createPriceSets([
{ {
rules: [{ rule_attribute: "region_id" }],
prices: [ prices: [
{ {
amount: 100, amount: 100,
@@ -499,11 +456,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
expect(priceSet).toEqual( expect(priceSet).toEqual(
expect.objectContaining({ expect.objectContaining({
rule_types: [
expect.objectContaining({
rule_attribute: "region_id",
}),
],
prices: expect.arrayContaining([ prices: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
amount: 100, amount: 100,
@@ -518,44 +470,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
) )
}) })
it("should create a price set with rule types and money amounts", async () => {
const [priceSet] = await service.createPriceSets([
{
rules: [{ rule_attribute: "region_id" }],
prices: [
{
amount: 100,
currency_code: "USD",
rules: {
region_id: "10",
},
},
],
},
])
expect(priceSet).toEqual(
expect.objectContaining({
rule_types: [
expect.objectContaining({
rule_attribute: "region_id",
}),
],
prices: [
expect.objectContaining({
amount: 100,
currency_code: "USD",
}),
],
price_rules: [
expect.objectContaining({
value: "10",
}),
],
})
)
})
it("should create a priceSet successfully", async () => { it("should create a priceSet successfully", async () => {
await service.createPriceSets([ await service.createPriceSets([
{ {
@@ -575,92 +489,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}) })
}) })
describe("removeRules", () => {
it("should delete prices for a price set associated to the rules that are deleted", async () => {
const createdPriceSet = await service.createPriceSets([
{
rules: [
{ rule_attribute: "region_id" },
{ rule_attribute: "currency_code" },
],
prices: [
{
currency_code: "EUR",
amount: 100,
rules: {
region_id: "test-region",
currency_code: "test-currency",
},
},
{
currency_code: "EUR",
amount: 500,
rules: {
currency_code: "test-currency",
},
},
],
},
])
await service.removeRules([
{ id: createdPriceSet[0].id, rules: ["region_id"] },
])
let priceSet = await service.listPriceSets(
{ id: [createdPriceSet[0].id] },
{ relations: ["rule_types", "prices", "price_rules"] }
)
expect(
expect.arrayContaining(
expect.objectContaining({
id: priceSet[0].id,
price_rules: [
{
id: expect.any(String),
rule_type: expect.objectContaining({
rule_attribute: "currency_code",
}),
},
],
prices: [
expect.objectContaining({
amount: 500,
currency_code: "EUR",
}),
],
rule_types: [
expect.objectContaining({
rule_attribute: "currency_code",
}),
],
})
)
)
await service.removeRules([
{ id: createdPriceSet[0].id, rules: ["currency_code"] },
])
priceSet = await service.listPriceSets(
{ id: [createdPriceSet[0].id] },
{ relations: ["rule_types", "prices", "price_rules"] }
)
expect(priceSet).toEqual([
{
id: expect.any(String),
price_rules: [],
prices: [],
rule_types: [],
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
},
])
})
})
describe("addPrices", () => { describe("addPrices", () => {
it("should add prices to existing price set", async () => { it("should add prices to existing price set", async () => {
await service.addPrices([ await service.addPrices([
@@ -784,71 +612,6 @@ moduleIntegrationTestRunner<IPricingModuleService>({
expect(error.message).toEqual("Rule types don't exist for: city") expect(error.message).toEqual("Rule types don't exist for: city")
}) })
}) })
describe("addRules", () => {
it("should add rules to existing price set", async () => {
await service.addRules([
{
priceSetId: "price-set-1",
rules: [{ attribute: "region_id" }],
},
])
const [priceSet] = await service.listPriceSets(
{ id: ["price-set-1"] },
{ relations: ["rule_types"] }
)
expect(priceSet).toEqual(
expect.objectContaining({
id: "price-set-1",
rule_types: expect.arrayContaining([
expect.objectContaining({
rule_attribute: "currency_code",
}),
expect.objectContaining({
rule_attribute: "region_id",
}),
]),
})
)
})
it("should fail to add rules to non-existent price sets", async () => {
let error
try {
await service.addRules([
{
priceSetId: "price-set-doesn't-exist",
rules: [{ attribute: "region_id" }],
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
"PriceSets with ids: price-set-doesn't-exist was not found"
)
})
it("should fail to add rules with non-existent attributes", async () => {
let error
try {
await service.addRules([
{ priceSetId: "price-set-1", rules: [{ attribute: "city" }] },
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
"Rule types don't exist for attributes: city"
)
})
})
}) })
}, },
}) })
@@ -43,16 +43,15 @@ import {
PriceListRuleValue, PriceListRuleValue,
PriceRule, PriceRule,
PriceSet, PriceSet,
PriceSetRuleType,
RuleType, RuleType,
} from "@models" } from "@models"
import { PriceListService, RuleTypeService } from "@services" import { PriceListService, RuleTypeService } from "@services"
import { ServiceTypes } from "@types"
import { eventBuilders, validatePriceListDates } from "@utils" import { eventBuilders, validatePriceListDates } from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { PriceListIdPrefix } from "../models/price-list" import { PriceListIdPrefix } from "../models/price-list"
import { PriceSetIdPrefix } from "../models/price-set" import { PriceSetIdPrefix } from "../models/price-set"
import { ServiceTypes } from "@types"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -74,7 +73,6 @@ const generateMethodForModels = {
PriceListRuleValue, PriceListRuleValue,
PriceRule, PriceRule,
Price, Price,
PriceSetRuleType,
RuleType, RuleType,
} }
@@ -102,7 +100,6 @@ export default class PricingModuleService
protected readonly ruleTypeService_: RuleTypeService protected readonly ruleTypeService_: RuleTypeService
protected readonly priceSetService_: ModulesSdkTypes.IMedusaInternalService<PriceSet> protected readonly priceSetService_: ModulesSdkTypes.IMedusaInternalService<PriceSet>
protected readonly priceRuleService_: ModulesSdkTypes.IMedusaInternalService<PriceRule> protected readonly priceRuleService_: ModulesSdkTypes.IMedusaInternalService<PriceRule>
protected readonly priceSetRuleTypeService_: ModulesSdkTypes.IMedusaInternalService<PriceSetRuleType>
protected readonly priceService_: ModulesSdkTypes.IMedusaInternalService<Price> protected readonly priceService_: ModulesSdkTypes.IMedusaInternalService<Price>
protected readonly priceListService_: PriceListService protected readonly priceListService_: PriceListService
protected readonly priceListRuleService_: ModulesSdkTypes.IMedusaInternalService<PriceListRule> protected readonly priceListRuleService_: ModulesSdkTypes.IMedusaInternalService<PriceListRule>
@@ -115,7 +112,6 @@ export default class PricingModuleService
ruleTypeService, ruleTypeService,
priceSetService, priceSetService,
priceRuleService, priceRuleService,
priceSetRuleTypeService,
priceService, priceService,
priceListService, priceListService,
priceListRuleService, priceListRuleService,
@@ -132,7 +128,6 @@ export default class PricingModuleService
this.priceSetService_ = priceSetService this.priceSetService_ = priceSetService
this.ruleTypeService_ = ruleTypeService this.ruleTypeService_ = ruleTypeService
this.priceRuleService_ = priceRuleService this.priceRuleService_ = priceRuleService
this.priceSetRuleTypeService_ = priceSetRuleTypeService
this.priceService_ = priceService this.priceService_ = priceService
this.priceListService_ = priceListService this.priceListService_ = priceListService
this.priceListRuleService_ = priceListRuleService this.priceListRuleService_ = priceListRuleService
@@ -497,9 +492,7 @@ export default class PricingModuleService
const { entities: upsertedPrices } = const { entities: upsertedPrices } =
await this.priceService_.upsertWithReplace( await this.priceService_.upsertWithReplace(
prices, prices,
{ { relations: ["price_rules"] },
relations: ["price_rules"],
},
sharedContext sharedContext
) )
@@ -527,37 +520,6 @@ export default class PricingModuleService
return priceSets return priceSets
} }
async addRules(
data: PricingTypes.AddRulesDTO,
sharedContext?: Context
): Promise<PricingTypes.PriceSetDTO>
async addRules(
data: PricingTypes.AddRulesDTO[],
sharedContext?: Context
): Promise<PricingTypes.PriceSetDTO[]>
@InjectManager("baseRepository_")
async addRules(
data: PricingTypes.AddRulesDTO | PricingTypes.AddRulesDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceSetDTO[] | PricingTypes.PriceSetDTO> {
const inputs = Array.isArray(data) ? data : [data]
const priceSets = await this.addRules_(inputs, sharedContext)
const dbPriceSets = await this.listPriceSets(
{ id: priceSets.map(({ id }) => id) },
{ relations: ["rule_types"] }
)
const orderedPriceSets = priceSets.map((priceSet) => {
return dbPriceSets.find((p) => p.id === priceSet.id)!
})
return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0]
}
async addPrices( async addPrices(
data: AddPricesDTO, data: AddPricesDTO,
sharedContext?: Context sharedContext?: Context
@@ -591,50 +553,6 @@ export default class PricingModuleService
return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0] return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0]
} }
@InjectTransactionManager("baseRepository_")
async removeRules(
data: PricingTypes.RemovePriceSetRulesDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const priceSets = await this.priceSetService_.list(
{ id: data.map((d) => d.id) },
{},
sharedContext
)
const priceSetIds = priceSets.map((ps) => ps.id)
const ruleTypes = await this.ruleTypeService_.list(
{
rule_attribute: data.map((d) => d.rules || []).flat(),
},
{ take: null },
sharedContext
)
const ruleTypeIds = ruleTypes.map((rt) => rt.id)
const priceSetRuleTypes = await this.priceSetRuleTypeService_.list(
{ price_set_id: priceSetIds, rule_type_id: ruleTypeIds },
{ take: null },
sharedContext
)
const priceRules = await this.priceRuleService_.list(
{ price_set_id: priceSetIds, rule_type_id: ruleTypeIds },
{ select: ["price"], take: null },
sharedContext
)
await this.priceSetRuleTypeService_.delete(
priceSetRuleTypes.map((psrt) => psrt.id),
sharedContext
)
await this.priceService_.delete(
priceRules.map((pr) => pr.price.id),
sharedContext
)
}
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents() @EmitEvents()
// @ts-ignore // @ts-ignore
@@ -723,7 +641,12 @@ export default class PricingModuleService
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const ruleAttributes = deduplicate( const ruleAttributes = deduplicate(
data.map((d) => d.rules?.map((r) => r.rule_attribute) ?? []).flat() data
.map(
(d) =>
d.prices?.map((ma) => Object.keys(ma?.rules ?? {})).flat() ?? []
)
.flat()
) )
const ruleTypes = await this.ruleTypeService_.list( const ruleTypes = await this.ruleTypeService_.list(
@@ -744,56 +667,27 @@ export default class PricingModuleService
if (invalidRuleAttributes.length > 0) { if (invalidRuleAttributes.length > 0) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
`Rule types don't exist for: ${invalidRuleAttributes.join(", ")}` `Rule types don't exist for prices with rule attribute: ${invalidRuleAttributes.join(
)
}
const invalidMoneyAmountRule = data
.map(
(d) => d.prices?.map((ma) => Object.keys(ma?.rules ?? {})).flat() ?? []
)
.flat()
.filter((r) => !ruleTypeMap.has(r))
if (invalidMoneyAmountRule.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Rule types don't exist for money amounts with rule attribute: ${invalidMoneyAmountRule.join(
", " ", "
)}` )}`
) )
} }
const ruleSetRuleTypeToCreateMap: Map<string, PriceSetRuleType> = new Map()
const toCreate = input.map((inputData) => { const toCreate = input.map((inputData) => {
const id = generateEntityId( const id = generateEntityId(
(inputData as unknown as PriceSet).id, (inputData as unknown as PriceSet).id,
PriceSetIdPrefix PriceSetIdPrefix
) )
const { prices, rules = [], ...rest } = inputData const { prices, ...rest } = inputData
let pricesData: CreatePricesDTO[] = [] let pricesData: CreatePricesDTO[] = []
rules.forEach((rule) => {
const priceSetRuleType = {
rule_type_id: ruleTypeMap.get(rule.rule_attribute).id,
price_set_id: id,
} as PriceSetRuleType
ruleSetRuleTypeToCreateMap.set(
JSON.stringify(priceSetRuleType),
priceSetRuleType
)
})
if (inputData.prices) { if (inputData.prices) {
pricesData = inputData.prices.map((price) => { pricesData = inputData.prices.map((price) => {
let { rules: priceRules = {}, ...rest } = price let { rules: priceRules = {}, ...rest } = price
const cleanRules = priceRules ? removeNullish(priceRules) : {} const cleanRules = priceRules ? removeNullish(priceRules) : {}
const numberOfRules = Object.keys(cleanRules).length const numberOfRules = Object.keys(cleanRules).length
const rulesDataMap = new Map() const rulesDataMap = new Map()
Object.entries(priceRules).map(([attribute, value]) => { Object.entries(priceRules).map(([attribute, value]) => {
@@ -803,16 +697,6 @@ export default class PricingModuleService
value, value,
} }
rulesDataMap.set(JSON.stringify(rule), rule) rulesDataMap.set(JSON.stringify(rule), rule)
const priceSetRuleType = {
rule_type_id: ruleTypeMap.get(attribute).id,
price_set_id: id,
} as PriceSetRuleType
ruleSetRuleTypeToCreateMap.set(
JSON.stringify(priceSetRuleType),
priceSetRuleType
)
}) })
return { return {
@@ -880,99 +764,9 @@ export default class PricingModuleService
sharedContext, sharedContext,
}) })
if (ruleSetRuleTypeToCreateMap.size) {
await this.priceSetRuleTypeService_.create(
Array.from(ruleSetRuleTypeToCreateMap.values()),
sharedContext
)
}
return createdPriceSets return createdPriceSets
} }
@InjectTransactionManager("baseRepository_")
protected async addRules_(
inputs: PricingTypes.AddRulesDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PriceSet[]> {
const priceSets = await this.priceSetService_.list(
{ id: inputs.map((d) => d.priceSetId) },
{ relations: ["rule_types"] },
sharedContext
)
const priceSetRuleTypeMap: Map<string, Map<string, RuleTypeDTO>> = new Map(
priceSets.map((priceSet) => [
priceSet.id,
new Map([...priceSet.rule_types].map((rt) => [rt.rule_attribute, rt])),
])
)
const priceSetMap = new Map(priceSets.map((p) => [p.id, p]))
const invalidPriceSetInputs = inputs.filter(
(d) => !priceSetMap.has(d.priceSetId)
)
if (invalidPriceSetInputs.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`PriceSets with ids: ${invalidPriceSetInputs
.map((d) => d.priceSetId)
.join(", ")} was not found`
)
}
const ruleTypes = await this.ruleTypeService_.list(
{
rule_attribute: inputs
.map((data) => data.rules.map((r) => r.attribute))
.flat(),
},
{ take: null },
sharedContext
)
const ruleTypeMap: Map<string, RuleTypeDTO> = new Map(
ruleTypes.map((rt) => [rt.rule_attribute, rt])
)
const invalidRuleAttributeInputs = inputs
.map((d) => d.rules.map((r) => r.attribute))
.flat()
.filter((r) => !ruleTypeMap.has(r))
if (invalidRuleAttributeInputs.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Rule types don't exist for attributes: ${[
...new Set(invalidRuleAttributeInputs),
].join(", ")}`
)
}
const priceSetRuleTypesCreate: PricingTypes.CreatePriceSetRuleTypeDTO[] = []
inputs.forEach((data) => {
for (const rule of data.rules) {
if (priceSetRuleTypeMap.get(data.priceSetId)!.has(rule.attribute)) {
continue
}
priceSetRuleTypesCreate.push({
rule_type_id: ruleTypeMap.get(rule.attribute)!.id,
price_set_id: priceSetMap.get(data.priceSetId)!.id,
})
}
})
await this.priceSetRuleTypeService_.create(
priceSetRuleTypesCreate,
sharedContext
)
return priceSets
}
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
protected async addPrices_( protected async addPrices_(
input: AddPricesDTO[], input: AddPricesDTO[],
@@ -1365,9 +1159,6 @@ export default class PricingModuleService
const ruleTypeAttributes: string[] = [] const ruleTypeAttributes: string[] = []
const priceListIds: string[] = [] const priceListIds: string[] = []
const priceIds: string[] = [] const priceIds: string[] = []
const priceSetIds = data
.map((d) => d.prices.map((price) => price.price_set_id))
.flat()
for (const priceListData of data) { for (const priceListData of data) {
priceListIds.push(priceListData.price_list_id) priceListIds.push(priceListData.price_list_id)
@@ -1398,53 +1189,6 @@ export default class PricingModuleService
ruleTypes.map((rt) => [rt.rule_attribute, rt]) ruleTypes.map((rt) => [rt.rule_attribute, rt])
) )
const priceSets = await this.listPriceSets(
{ id: priceSetIds },
{ relations: ["rule_types"] },
sharedContext
)
const priceSetRuleTypeMap: Map<string, Set<string>> = priceSets.reduce(
(acc, curr) => {
const priceSetRuleAttributeSet: Set<string> =
acc.get(curr.id) || new Set()
for (const rt of curr.rule_types ?? []) {
priceSetRuleAttributeSet.add(rt.rule_attribute)
}
acc.set(curr.id, priceSetRuleAttributeSet)
return acc
},
new Map()
)
const ruleTypeErrors: string[] = []
for (const priceListData of data) {
for (const price of priceListData.prices) {
for (const ruleAttribute of Object.keys(price.rules ?? {})) {
if (
!priceSetRuleTypeMap.get(price.price_set_id)?.has(ruleAttribute)
) {
ruleTypeErrors.push(
`rule_attribute "${ruleAttribute}" in price set ${price.price_set_id}`
)
}
}
}
}
if (ruleTypeErrors.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid rule type configuration: Price set rules doesn't exist for ${ruleTypeErrors.join(
", "
)}`
)
}
const priceLists = await this.listPriceLists( const priceLists = await this.listPriceLists(
{ id: priceListIds }, { id: priceListIds },
{ take: null }, { take: null },
@@ -1532,52 +1276,6 @@ export default class PricingModuleService
sharedContext sharedContext
) )
const priceSets = await this.listPriceSets(
{ id: priceSetIds },
{ relations: ["rule_types"] },
sharedContext
)
const priceSetRuleTypeMap: Map<string, Set<string>> = priceSets.reduce(
(acc, curr) => {
const priceSetRuleAttributeSet: Set<string> =
acc.get(curr.id) || new Set()
for (const rt of curr.rule_types ?? []) {
priceSetRuleAttributeSet.add(rt.rule_attribute)
}
acc.set(curr.id, priceSetRuleAttributeSet)
return acc
},
new Map()
)
const ruleTypeErrors: string[] = []
for (const priceListData of data) {
for (const price of priceListData.prices) {
for (const rule_attribute of Object.keys(price.rules ?? {})) {
if (
!priceSetRuleTypeMap.get(price.price_set_id)?.has(rule_attribute)
) {
ruleTypeErrors.push(
`rule_attribute "${rule_attribute}" in price set ${price.price_set_id}`
)
}
}
}
}
if (ruleTypeErrors.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid rule type configuration: Price set rules doesn't exist for ${ruleTypeErrors.join(
", "
)}`
)
}
const ruleTypeMap: Map<string, RuleTypeDTO> = new Map( const ruleTypeMap: Map<string, RuleTypeDTO> = new Map(
ruleTypes.map((rt) => [rt.rule_attribute, rt]) ruleTypes.map((rt) => [rt.rule_attribute, rt])
) )