fix: Disallow creating duplicate prices (#7866)

* fix: Disallow creating duplicate prices

* fix: Don't pass id to manager create in upsertWithReplace
This commit is contained in:
Stevche Radevski
2024-07-02 17:06:58 +02:00
committed by GitHub
parent 87375db9ef
commit b4aa7fb9a7
12 changed files with 560 additions and 350 deletions

View File

@@ -113,7 +113,9 @@ medusaIntegrationTestRunner({
{
amount: 105,
currency_code: region1.currency_code,
region_id: region1.id,
rules: {
region_id: region1.id,
},
variant_id: product1.variants[0].id,
},
],
@@ -508,84 +510,61 @@ medusaIntegrationTestRunner({
)
})
it.skip("Adds a batch of new prices to a price list overriding existing prices", async () => {
// BREAKING: There is no support for overriding configuration
it("Adds a batch of new prices to a price list overriding existing prices", async () => {
// BREAKING: The payload of the batch request changed
// BREAKING: The create dataset does an upsert (before an explicit `override` flag was passed)
const payload = {
prices: [
create: [
{
amount: 45,
currency_code: "usd",
variant_id: "test-variant",
min_quantity: 1001,
max_quantity: 2000,
variant_id: product1.variants[0].id,
min_quantity: 1,
max_quantity: 100,
},
{
amount: 35,
currency_code: "usd",
variant_id: "test-variant",
min_quantity: 2001,
max_quantity: 3000,
},
{
amount: 25,
currency_code: "usd",
variant_id: "test-variant",
min_quantity: 3001,
max_quantity: 4000,
variant_id: product1.variants[0].id,
min_quantity: 101,
max_quantity: 500,
},
],
override: true,
}
const response = await api.post(
"/admin/price-lists/pl_no_customer_groups/prices/batch",
await api.post(
`/admin/price-lists/${pricelist1.id}/prices/batch`,
payload,
adminHeaders
)
const response = await api.get(
`/admin/price-lists/${pricelist1.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.price_list.prices.length).toEqual(3)
expect(response.data.price_list.prices).toMatchSnapshot([
{
id: expect.any(String),
price_list_id: "pl_no_customer_groups",
amount: 45,
currency_code: "usd",
variant_id: "test-variant",
min_quantity: 1001,
max_quantity: 2000,
created_at: expect.any(String),
updated_at: expect.any(String),
variant: expect.any(Object),
variants: expect.any(Array),
},
{
id: expect.any(String),
price_list_id: "pl_no_customer_groups",
amount: 35,
currency_code: "usd",
variant_id: "test-variant",
min_quantity: 2001,
max_quantity: 3000,
created_at: expect.any(String),
updated_at: expect.any(String),
variant: expect.any(Object),
variants: expect.any(Array),
},
{
id: expect.any(String),
price_list_id: "pl_no_customer_groups",
amount: 25,
currency_code: "usd",
variant_id: "test-variant",
min_quantity: 3001,
max_quantity: 4000,
created_at: expect.any(String),
updated_at: expect.any(String),
variant: expect.any(Object),
variants: expect.any(Array),
},
])
expect(response.data.price_list.prices.length).toEqual(2)
expect(response.data.price_list.prices).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
amount: 45,
currency_code: "usd",
min_quantity: "1",
max_quantity: "100",
variant_id: product1.variants[0].id,
}),
expect.objectContaining({
id: expect.any(String),
amount: 35,
currency_code: "usd",
min_quantity: "101",
max_quantity: "500",
variant_id: product1.variants[0].id,
}),
])
)
})
it("Adds a batch of new prices where a MA record have a `region_id` instead of `currency_code`", async () => {
@@ -596,7 +575,9 @@ medusaIntegrationTestRunner({
amount: 100,
variant_id: product1.variants[0].id,
currency_code: "eur",
region_id: region1.id,
rules: {
region_id: region1.id,
},
},
{
amount: 200,
@@ -640,7 +621,7 @@ medusaIntegrationTestRunner({
id: expect.any(String),
amount: 100,
currency_code: "eur",
// region_id: region1.id,
rules: { region_id: region1.id },
variant_id: product1.variants[0].id,
}),
expect.objectContaining({

View File

@@ -2,6 +2,7 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
AddPriceListPricesDTO,
CreatePriceListPriceDTO,
CreatePriceListPriceWorkflowDTO,
CreatePriceListPricesWorkflowStepDTO,
IPricingModuleService,
} from "@medusajs/types"
@@ -22,10 +23,12 @@ export const createPriceListPricesStep = createStep(
const pricesToAdd: CreatePriceListPriceDTO[] = []
for (const price of prices) {
pricesToAdd.push({
const toPush = {
...price,
price_set_id: variantPriceSetMap[price.variant_id!],
})
} as CreatePriceListPriceDTO
delete (toPush as Partial<CreatePriceListPriceWorkflowDTO>).variant_id
pricesToAdd.push(toPush)
}
if (pricesToAdd.length) {

View File

@@ -4,6 +4,7 @@ import {
PriceDTO,
UpdatePriceListPriceDTO,
UpdatePriceListPricesDTO,
UpdatePriceListPriceWorkflowDTO,
UpdatePriceListPriceWorkflowStepDTO,
} from "@medusajs/types"
import { buildPriceSetPricesForModule } from "@medusajs/utils"
@@ -25,10 +26,13 @@ export const updatePriceListPricesStep = createStep(
const { prices = [], id } = priceListData
for (const price of prices) {
pricesToUpdate.push({
const toPush = {
...price,
price_set_id: variantPriceSetMap[price.variant_id!],
})
} as UpdatePriceListPriceDTO
delete (toPush as Partial<UpdatePriceListPriceWorkflowDTO>).variant_id
pricesToUpdate.push(toPush)
if (price.id) {
priceIds.push(price.id)

View File

@@ -747,16 +747,22 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
entityName: string,
data: any
): Record<string, any> & { id: string } {
const created = manager.create(entityName, data, {
managed: false,
persist: false,
})
// We set the id to undefined to make sure the entity isn't fetched from the entity map if it is an update,
// giving us incorrect data for the bignumberdata field (I though managed: false and persist: false would already do this)
const created = manager.create(
entityName,
{ ...data, id: undefined },
{
managed: false,
persist: false,
}
)
const resp = {
// `create` will omit non-existent fields, but we want to pass the data the user provided through so the correct errors get thrown
...data,
...(created as any).__helper.__bignumberdata,
id: (created as any).id,
id: data.id ?? (created as any).id,
}
// Non-persist relation columns should be removed before we do the upsert.

View File

@@ -799,7 +799,7 @@ moduleIntegrationTestRunner<IPricingModuleService>({
{
id: "price-set-PLN",
is_calculated_price_price_list: true,
calculated_amount: 116,
calculated_amount: 232,
is_original_price_price_list: false,
original_amount: 400,
currency_code: "PLN",

View File

@@ -586,6 +586,63 @@ moduleIntegrationTestRunner<IPricingModuleService>({
})
)
})
it("should take the later price when passing two equivalent prices twice", async () => {
const [created] = await service.createPriceLists([
{
title: "test",
description: "test",
starts_at: "10/10/2010",
ends_at: "10/20/2030",
prices: [
{
amount: 400,
currency_code: "EUR",
price_set_id: "price-set-1",
rules: {
region_id: "DE",
},
},
{
amount: 600,
currency_code: "EUR",
price_set_id: "price-set-1",
rules: {
region_id: "DE",
},
},
],
},
])
const [priceList] = await service.listPriceLists(
{
id: [created.id],
},
{
relations: ["prices", "prices.price_rules"],
}
)
expect(priceList).toEqual(
expect.objectContaining({
id: expect.any(String),
prices: [
expect.objectContaining({
rules_count: 1,
price_rules: [
expect.objectContaining({
id: expect.any(String),
value: "DE",
}),
],
amount: 600,
currency_code: "EUR",
}),
],
})
)
})
})
describe("addPriceListPrices", () => {
@@ -810,6 +867,69 @@ moduleIntegrationTestRunner<IPricingModuleService>({
})
)
})
it("should do an update to the price if an equivalent price already exists", async () => {
const [priceSet] = await service.createPriceSets([{}])
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",
},
},
],
},
])
await service.addPriceListPrices([
{
price_list_id: "price-list-1",
prices: [
{
id: "test-price-id",
amount: 234,
currency_code: "EUR",
price_set_id: priceSet.id,
rules: {
region_id: "test",
},
},
],
},
])
const [priceList] = await service.listPriceLists(
{ id: ["price-list-1"] },
{
relations: ["prices", "prices.price_rules"],
}
)
expect(priceList).toEqual(
expect.objectContaining({
id: expect.any(String),
prices: expect.arrayContaining([
expect.objectContaining({
price_rules: [
expect.objectContaining({
value: "test",
attribute: "region_id",
}),
],
amount: 234,
currency_code: "EUR",
}),
]),
})
)
})
})
describe("removePrices", () => {

View File

@@ -398,6 +398,40 @@ moduleIntegrationTestRunner<IPricingModuleService>({
])
)
})
it("should upsert the later price when setting a price set with existing equivalent rules", async () => {
await service.updatePriceSets(id, {
prices: [
{
amount: 100,
currency_code: "USD",
rules: { region_id: "1234" },
},
{
amount: 200,
currency_code: "USD",
rules: { region_id: "1234" },
},
],
})
const priceSet = await service.retrievePriceSet(id, {
relations: ["prices", "prices.price_rules"],
})
expect(priceSet.prices).toEqual([
expect.objectContaining({
amount: 200,
currency_code: "USD",
price_rules: [
expect.objectContaining({
attribute: "region_id",
value: "1234",
}),
],
}),
])
})
})
describe("create", () => {
@@ -512,6 +546,43 @@ moduleIntegrationTestRunner<IPricingModuleService>({
})
)
})
it("should take the later price when passing two prices with equivalent rules", async () => {
await service.createPriceSets([
{
id: "price-set-new",
prices: [
{
amount: 100,
currency_code: "USD",
rules: { region_id: "1234" },
},
{
amount: 200,
currency_code: "USD",
rules: { region_id: "1234" },
},
],
} as unknown as CreatePriceSetDTO,
])
const priceSet = await service.retrievePriceSet("price-set-new", {
relations: ["prices", "prices.price_rules"],
})
expect(priceSet.prices).toEqual([
expect.objectContaining({
amount: 200,
currency_code: "USD",
price_rules: [
expect.objectContaining({
attribute: "region_id",
value: "1234",
}),
],
}),
])
})
})
describe("addPrices", () => {
@@ -523,7 +594,7 @@ moduleIntegrationTestRunner<IPricingModuleService>({
{
amount: 100,
currency_code: "USD",
rules: { currency_code: "USD" },
rules: { region_id: "1234" },
},
],
},
@@ -574,7 +645,7 @@ moduleIntegrationTestRunner<IPricingModuleService>({
{
amount: 100,
currency_code: "USD",
rules: { currency_code: "USD" },
rules: { region_id: "region-1" },
},
],
},
@@ -616,6 +687,57 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}),
])
})
it("should do an update if a price exists with the equivalent rules", async () => {
await service.addPrices([
{
priceSetId: "price-set-1",
prices: [
{
amount: 100,
currency_code: "USD",
rules: { region_id: "123" },
},
],
},
])
await service.addPrices([
{
priceSetId: "price-set-1",
prices: [
{
amount: 200,
currency_code: "USD",
rules: { region_id: "123" },
},
],
},
])
const priceSet = await service.retrievePriceSet("price-set-1", {
relations: ["prices", "prices.price_rules"],
})
expect(
priceSet.prices?.sort((a: any, b: any) => a.amount - b.amount)
).toEqual([
expect.objectContaining({
amount: 200,
currency_code: "USD",
price_rules: [
expect.objectContaining({
attribute: "region_id",
value: "123",
}),
],
}),
expect.objectContaining({
amount: 500,
currency_code: "USD",
}),
])
})
})
})
},

View File

@@ -163,6 +163,9 @@ export class PricingRepository
pl_rules_count: "price.pl_rules_count",
price_list_type: "price.pl_type",
price_list_id: "price.price_list_id",
all_rules_count: knex.raw(
"COALESCE(price.rules_count, 0) + COALESCE(price.pl_rules_count, 0)"
),
})
.join(priceSubQueryKnex.as("price"), "price.price_set_id", "ps.id")
.leftJoin("price_rule as pr", "pr.price_id", "price.id")
@@ -171,8 +174,8 @@ export class PricingRepository
.orderBy([
{ column: "price.has_price_list", order: "asc" },
{ column: "all_rules_count", order: "desc" },
{ column: "amount", order: "asc" },
{ column: "rules_count", order: "desc" },
])
if (quantity) {

View File

@@ -9,7 +9,6 @@ import {
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
PriceDTO,
PriceSetDTO,
PricingContext,
PricingFilters,
@@ -32,6 +31,7 @@ import {
PriceListType,
promiseAll,
removeNullish,
simpleHash,
} from "@medusajs/utils"
import { Price, PriceList, PriceListRule, PriceRule, PriceSet } from "@models"
@@ -39,7 +39,7 @@ import { Price, PriceList, PriceListRule, PriceRule, PriceSet } from "@models"
import { ServiceTypes } from "@types"
import { eventBuilders, validatePriceListDates } from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { CreatePriceListDTO } from "src/types/services"
import { CreatePriceListDTO, UpsertPriceDTO } from "src/types/services"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -426,35 +426,6 @@ export default class PricingModuleService
return isString(idOrSelector) ? priceSets[0] : priceSets
}
private async normalizeUpdateData(data: ServiceTypes.UpdatePriceSetInput[]) {
return data.map((priceSet) => {
const prices = priceSet.prices?.map((price) => {
const rules = Object.entries(price.rules ?? {}).map(
([attribute, value]) => {
return {
attribute,
value,
}
}
)
const hasRulesInput = isPresent(price.rules)
delete price.rules
return {
...price,
price_set_id: priceSet.id,
price_rules: hasRulesInput ? rules : undefined,
rules_count: hasRulesInput ? rules.length : undefined,
}
})
return {
...priceSet,
prices,
}
})
}
@InjectTransactionManager("baseRepository_")
protected async updatePriceSets_(
data: ServiceTypes.UpdatePriceSetInput[],
@@ -496,6 +467,67 @@ export default class PricingModuleService
return priceSets
}
private async normalizeUpdateData(data: ServiceTypes.UpdatePriceSetInput[]) {
return data.map((priceSet) => {
return {
...priceSet,
prices: this.normalizePrices(
priceSet.prices?.map((p) => ({ ...p, price_set_id: priceSet.id })),
[]
),
}
})
}
private normalizePrices(
data: CreatePricesDTO[] | undefined,
existingPrices: PricingTypes.PriceDTO[],
priceListId?: string | undefined
) {
const pricesToUpsert = new Map<
string,
CreatePricesDTO & { price_rules?: CreatePriceRuleDTO[] }
>()
const existingPricesMap = new Map<string, PricingTypes.PriceDTO>()
existingPrices?.forEach((price) => {
existingPricesMap.set(hashPrice(price), price)
})
data?.forEach((price) => {
const cleanRules = price.rules ? removeNullish(price.rules) : {}
const ruleEntries = Object.entries(cleanRules)
const rules = ruleEntries.map(([attribute, value]) => {
return {
attribute,
value,
}
})
const hasRulesInput = isPresent(price.rules)
const entry = {
...price,
price_list_id: priceListId,
price_rules: hasRulesInput ? rules : undefined,
rules_count: hasRulesInput ? ruleEntries.length : undefined,
} as UpsertPriceDTO
delete (entry as CreatePricesDTO).rules
const entryHash = hashPrice(entry)
// We want to keep the existing rules as they might already have ids, but any other data should come from the updated input
const existing = existingPricesMap.get(entryHash)
pricesToUpsert.set(entryHash, {
...entry,
id: existing?.id ?? entry.id,
price_rules: existing?.price_rules ?? entry.price_rules,
})
return entry
})
return Array.from(pricesToUpsert.values())
}
async addPrices(
data: AddPricesDTO,
sharedContext?: Context
@@ -619,35 +651,8 @@ export default class PricingModuleService
const toCreate = input.map((inputData) => {
const entry = {
...inputData,
prices: this.normalizePrices(inputData.prices, []),
}
if (!inputData.prices) {
return entry
}
const pricesData: CreatePricesDTO[] = inputData.prices.map((price) => {
let { rules: priceRules = {}, ...rest } = price
const cleanRules = priceRules ? removeNullish(priceRules) : {}
const rules = Object.entries(cleanRules)
const numberOfRules = rules.length
const rulesDataMap = new Map()
rules.map(([attribute, value]) => {
const rule = {
attribute,
value,
}
rulesDataMap.set(JSON.stringify(rule), rule)
})
return {
...rest,
rules_count: numberOfRules,
price_rules: Array.from(rulesDataMap.values()),
}
})
entry.prices = pricesData
return entry
})
@@ -710,86 +715,74 @@ export default class PricingModuleService
) {
const priceSets = await this.listPriceSets(
{ id: input.map((d) => d.priceSetId) },
{},
{ take: null, relations: ["prices", "prices.price_rules"] },
sharedContext
)
const priceSetMap = new Map(priceSets.map((p) => [p.id, p]))
input.forEach(({ priceSetId }) => {
const priceSet = priceSetMap.get(priceSetId)
const existingPrices = priceSets
.map((p) => p.prices)
.flat() as PricingTypes.PriceDTO[]
const pricesToUpsert = input
.map((addPrice) =>
this.normalizePrices(
addPrice.prices?.map((p) => ({
...p,
price_set_id: addPrice.priceSetId,
})),
existingPrices
)
)
.filter(Boolean)
.flat() as UpsertPriceDTO[]
const priceSetMap = new Map<string, PriceSetDTO>(
priceSets.map((p) => [p.id, p])
)
pricesToUpsert.forEach((price) => {
const priceSet = priceSetMap.get(price.price_set_id)
if (!priceSet) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Price set with id: ${priceSetId} not found`
`Price set with id: ${price.price_set_id} not found`
)
}
})
const pricesToCreate: PricingTypes.CreatePriceDTO[] = input.flatMap(
({ priceSetId, prices }) =>
prices.map((price) => {
const numberOfRules = Object.entries(price?.rules ?? {}).length
const priceRules = Object.entries(price.rules ?? {}).map(
([attribute, value]) => ({
price_set_id: priceSetId,
attribute: attribute,
value,
})
)
return {
...price,
price_set_id: priceSetId,
rules_count: numberOfRules,
price_rules: priceRules,
}
})
)
const prices = await this.priceService_.create(
pricesToCreate,
sharedContext
)
/**
* Preparing data for emitting events
*/
const eventsData = prices.reduce(
(eventsData, price) => {
eventsData.prices.push({
id: price.id,
})
price.price_rules.map((priceRule) => {
eventsData.priceRules.push({
id: priceRule.id,
})
})
return eventsData
},
{
priceRules: [],
prices: [],
} as {
priceRules: { id: string }[]
prices: { id: string }[]
}
)
/**
* Emitting events for all created entities
*/
const { entities, performedActions } =
await this.priceService_.upsertWithReplace(
pricesToUpsert,
{ relations: ["price_rules"] },
sharedContext
)
eventBuilders.createdPrice({
data: eventsData.prices,
data: performedActions.created[Price.name] ?? [],
sharedContext,
})
eventBuilders.createdPriceRule({
data: eventsData.priceRules,
eventBuilders.updatedPrice({
data: performedActions.updated[Price.name] ?? [],
sharedContext,
})
eventBuilders.deletedPrice({
data: performedActions.deleted[Price.name] ?? [],
sharedContext,
})
return prices
eventBuilders.createdPriceRule({
data: performedActions.created[PriceRule.name] ?? [],
sharedContext,
})
eventBuilders.updatedPriceRule({
data: performedActions.updated[PriceRule.name] ?? [],
sharedContext,
})
eventBuilders.deletedPriceRule({
data: performedActions.deleted[PriceRule.name] ?? [],
sharedContext,
})
return entities
}
@InjectTransactionManager("baseRepository_")
@@ -807,29 +800,10 @@ export default class PricingModuleService
} as CreatePriceListDTO
if (priceListData.prices) {
const pricesData = priceListData.prices.map((price) => {
let { rules: priceRules = {}, ...rest } = price
const cleanRules = priceRules ? removeNullish(priceRules) : {}
const rules = Object.entries(cleanRules)
const numberOfRules = rules.length
const rulesDataMap = new Map()
rules.map(([attribute, value]) => {
const rule = {
attribute,
value,
}
rulesDataMap.set(JSON.stringify(rule), rule)
})
return {
...rest,
rules_count: numberOfRules,
price_rules: Array.from(rulesDataMap.values()),
}
})
entry.prices = pricesData
entry.prices = this.normalizePrices(
priceListData.prices,
[]
) as UpsertPriceDTO[]
}
if (priceListData.rules) {
@@ -994,39 +968,29 @@ export default class PricingModuleService
data: PricingTypes.UpdatePriceListPricesDTO[],
sharedContext: Context = {}
): Promise<Price[]> {
const priceListIds: string[] = []
const priceIds: string[] = []
for (const priceListData of data) {
priceListIds.push(priceListData.price_list_id)
for (const price of priceListData.prices) {
priceIds.push(price.id)
}
}
const prices = await this.listPrices(
{ id: priceIds },
{ take: null, relations: ["price_rules"] },
sharedContext
)
const priceMap: Map<string, PricingTypes.PriceDTO> = new Map(
prices.map((price) => [price.id, price])
)
const priceLists = await this.listPriceLists(
{ id: priceListIds },
{ take: null },
{ id: data.map((p) => p.price_list_id) },
{ take: null, relations: ["prices", "prices.price_rules"] },
sharedContext
)
const existingPrices = priceLists
.map((p) => p.prices ?? [])
.flat() as PricingTypes.PriceDTO[]
const pricesToUpsert = data
.map((addPrice) =>
this.normalizePrices(
addPrice.prices as UpsertPriceDTO[],
existingPrices,
addPrice.price_list_id
)
)
.filter(Boolean)
.flat() as UpsertPriceDTO[]
const priceListMap = new Map(priceLists.map((p) => [p.id, p]))
const pricesToUpdate: Partial<Price>[] = []
const priceRuleIdsToDelete: string[] = []
const priceRulesToCreate: CreatePriceRuleDTO[] = []
for (const { price_list_id: priceListId, prices } of data) {
const priceList = priceListMap.get(priceListId)
@@ -1036,38 +1000,15 @@ export default class PricingModuleService
`Price list with id: ${priceListId} not found`
)
}
for (const priceData of prices) {
const { rules = {}, price_set_id, ...rest } = priceData
const price = priceMap.get(rest.id)!
const priceRules = price.price_rules!
priceRulesToCreate.push(
...Object.entries(rules).map(([ruleAttribute, ruleValue]) => ({
price_set_id,
attribute: ruleAttribute,
value: ruleValue,
price_id: price.id,
}))
)
pricesToUpdate.push({
...rest,
rules_count: Object.keys(rules).length,
} as unknown as Price)
priceRuleIdsToDelete.push(...priceRules.map((pr) => pr.id))
}
}
const [_deletedPriceRule, _createdPriceRule, updatedPrices] =
await promiseAll([
this.priceRuleService_.delete(priceRuleIdsToDelete),
this.priceRuleService_.create(priceRulesToCreate),
this.priceService_.update(pricesToUpdate),
])
const { entities } = await this.priceService_.upsertWithReplace(
pricesToUpsert,
{ relations: ["price_rules"] },
sharedContext
)
return updatedPrices
return entities
}
@InjectTransactionManager("baseRepository_")
@@ -1083,97 +1024,73 @@ export default class PricingModuleService
data: PricingTypes.AddPriceListPricesDTO[],
sharedContext: Context = {}
): Promise<Price[]> {
const priceListIds: string[] = []
for (const priceListData of data) {
priceListIds.push(priceListData.price_list_id)
}
const priceLists = await this.listPriceLists(
{ id: priceListIds },
{},
{ id: data.map((p) => p.price_list_id) },
{ take: null, relations: ["prices", "prices.price_rules"] },
sharedContext
)
const existingPrices = priceLists
.map((p) => p.prices ?? [])
.flat() as PricingTypes.PriceDTO[]
const pricesToUpsert = data
.map((addPrice) =>
this.normalizePrices(
addPrice.prices,
existingPrices,
addPrice.price_list_id
)
)
.filter(Boolean)
.flat() as UpsertPriceDTO[]
const priceListMap = new Map(priceLists.map((p) => [p.id, p]))
const pricesToCreate: Partial<Price>[] = []
for (const { price_list_id: priceListId, prices } of data) {
const priceList = priceListMap.get(priceListId)
pricesToUpsert.forEach((price) => {
const priceList = priceListMap.get(price.price_list_id!)
if (!priceList) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Price list with id: ${priceListId} not found`
`Price list with id: ${price.price_list_id} not found`
)
}
})
const priceListPricesToCreate = prices.map((priceData) => {
const priceRules = priceData.rules || {}
const noOfRules = Object.keys(priceRules).length
const priceRulesToCreate = Object.entries(priceRules).map(
([ruleAttribute, ruleValue]) => {
return {
price_list_id: priceData.price_set_id,
attribute: ruleAttribute,
value: ruleValue,
}
}
)
return {
...priceData,
price_set_id: priceData.price_set_id,
title: "test",
price_list_id: priceList.id,
rules_count: noOfRules,
price_rules: priceRulesToCreate,
} as unknown as Price
})
pricesToCreate.push(...priceListPricesToCreate)
}
const createdPrices = await this.priceService_.create(
pricesToCreate,
sharedContext
)
const eventsData = createdPrices.reduce(
(eventsData, price) => {
eventsData.prices.push({
id: price.id,
})
price.price_rules.map((priceRule) => {
eventsData.priceRules.push({
id: priceRule.id,
})
})
return eventsData
},
{
priceRules: [],
prices: [],
} as {
priceRules: { id: string }[]
prices: { id: string }[]
}
)
const { entities, performedActions } =
await this.priceService_.upsertWithReplace(
pricesToUpsert,
{ relations: ["price_rules"] },
sharedContext
)
eventBuilders.createdPrice({
data: eventsData.prices,
data: performedActions.created[Price.name] ?? [],
sharedContext,
})
eventBuilders.createdPriceRule({
data: eventsData.priceRules,
eventBuilders.updatedPrice({
data: performedActions.updated[Price.name] ?? [],
sharedContext,
})
eventBuilders.deletedPrice({
data: performedActions.deleted[Price.name] ?? [],
sharedContext,
})
return createdPrices
eventBuilders.createdPriceRule({
data: performedActions.created[PriceRule.name] ?? [],
sharedContext,
})
eventBuilders.updatedPriceRule({
data: performedActions.updated[PriceRule.name] ?? [],
sharedContext,
})
eventBuilders.deletedPriceRule({
data: performedActions.deleted[PriceRule.name] ?? [],
sharedContext,
})
return entities
}
@InjectTransactionManager("baseRepository_")
@@ -1335,3 +1252,24 @@ export default class PricingModuleService
}
}
}
const hashPrice = (
price: PricingTypes.PriceDTO | PricingTypes.CreatePricesDTO
): string => {
const data = Object.entries({
currency_code: price.currency_code,
price_set_id: "price_set_id" in price ? price.price_set_id ?? null : null,
price_list_id:
"price_list_id" in price ? price.price_list_id ?? null : null,
min_quantity: price.min_quantity ? price.min_quantity.toString() : null,
max_quantity: price.max_quantity ? price.max_quantity.toString() : null,
...("price_rules" in price
? price.price_rules?.reduce((agg, pr) => {
agg[pr.attribute] = pr.value
return agg
}, {})
: {}),
}).sort(([a], [b]) => a.localeCompare(b))
return simpleHash(JSON.stringify(data))
}

View File

@@ -1,2 +1,3 @@
export * from "./price-list"
export * from "./price-set"
export * from "./price"

View File

@@ -0,0 +1,8 @@
import { PricingTypes } from "@medusajs/types"
export interface UpsertPriceDTO
extends Omit<PricingTypes.CreatePriceDTO, "rules"> {
id?: string
price_list_id?: string
price_rules: PricingTypes.CreatePriceRuleDTO[]
}

View File

@@ -42,4 +42,28 @@ export const eventBuilders = {
object: "price_list_rule",
eventsEnum: PricingEvents,
}),
updatedPrice: eventBuilderFactory({
source: Modules.PRICING,
action: CommonEvents.UPDATED,
object: "price",
eventsEnum: PricingEvents,
}),
updatedPriceRule: eventBuilderFactory({
source: Modules.PRICING,
action: CommonEvents.UPDATED,
object: "price_rule",
eventsEnum: PricingEvents,
}),
deletedPrice: eventBuilderFactory({
source: Modules.PRICING,
action: CommonEvents.DELETED,
object: "price",
eventsEnum: PricingEvents,
}),
deletedPriceRule: eventBuilderFactory({
source: Modules.PRICING,
action: CommonEvents.DELETED,
object: "price_rule",
eventsEnum: PricingEvents,
}),
}