fix(pricing, medusa): resolve minor pricing-module bugs (#5685)

* fix region price updates

* pricing migration: include title/name field value migration

* update migration scripts for pricing module

* move file to script utils

* rename file

* rename file

* add changeset

* remove redundant maps

* update migration script

* nit

* remove unnecessary variable

* filter before map

* array function naming cleanup

* chore: address pr reviews

---------

Co-authored-by: Philip Korsholm <philip.korsholm@hotmail.com>
Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
This commit is contained in:
Riqwan Thamir
2023-11-23 17:11:47 +01:00
committed by GitHub
parent 02ea9ac3ac
commit 1e39a95f8a
9 changed files with 404 additions and 337 deletions
@@ -1,40 +1,10 @@
import { AwilixContainer } from "awilix"
import { IPricingModuleService } from "@medusajs/types"
import { createDefaultRuleTypes } from "./utils/create-default-rule-types"
import dotenv from "dotenv"
import express from "express"
import loaders from "../loaders"
dotenv.config()
export const createDefaultRuleTypes = async (container: AwilixContainer) => {
const pricingModuleService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
const existing = await pricingModuleService.listRuleTypes(
{ rule_attribute: ["region_id", "customer_group_id"] },
{ take: 2 }
)
if (existing.length === 2) {
return
}
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 }) {
const app = express()
const { container } = await loaders({
@@ -1,146 +0,0 @@
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")
})
@@ -0,0 +1,250 @@
import { IPricingModuleService, PricingTypes } from "@medusajs/types"
import { promiseAll } from "@medusajs/utils"
import { AwilixContainer } from "awilix"
import dotenv from "dotenv"
import express from "express"
import loaders from "../loaders"
import Logger from "../loaders/logger"
import { PriceList } from "../models"
import { CurrencyService, PriceListService } from "../services"
import { createDefaultRuleTypes } from "./utils/create-default-rule-types"
import { migrateProductVariantPricing } from "./utils/migrate-money-amounts-to-pricing-module"
dotenv.config()
const BATCH_SIZE = 1000
const migratePriceLists = async (container: AwilixContainer) => {
const pricingModuleService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
let offset = 0
const priceListCoreService: PriceListService =
container.resolve("priceListService")
const remoteQuery = container.resolve("remoteQuery")
const [_, totalCount] = await priceListCoreService.listAndCount(
{},
{ select: ["id"] }
)
while (offset < totalCount) {
const corePriceLists = await priceListCoreService.list(
{},
{
take: BATCH_SIZE,
skip: offset,
relations: ["customer_groups", "prices", "prices.variants"],
}
)
const pricingModulePriceLists = await pricingModuleService.listPriceLists(
{ id: corePriceLists.map(({ id }) => id) },
{
take: BATCH_SIZE,
skip: offset,
select: ["id"],
}
)
const priceListIdsToUpdateSet = new Set<string>(
pricingModulePriceLists.map(({ id }) => id)
)
const priceListsToCreate: PriceList[] = []
const priceListsToUpdate: PriceList[] = []
const variantIds: string[] = []
for (const corePriceList of corePriceLists) {
if (priceListIdsToUpdateSet.has(corePriceList.id)) {
priceListsToCreate.push(corePriceList)
} else {
priceListsToUpdate.push(corePriceList)
}
const corePrices = corePriceList.prices || []
variantIds.push(
...corePrices.map((corePrice) => corePrice.variants?.[0]?.id)
)
}
const query = {
product_variant_price_set: {
__args: {
variant_id: variantIds,
},
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIdPriceSetIdMap = new Map<string, string>(
variantPriceSets.map((vps) => [vps.variant_id, vps.price_set_id])
)
const promises: Promise<any>[] = []
if (priceListsToUpdate.length) {
await pricingModuleService.updatePriceLists(
priceListsToUpdate.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(({ id }) => id),
}
}
return updateData
})
)
promises.push(
pricingModuleService.addPriceListPrices(
priceListsToUpdate.map((priceList) => {
return {
priceListId: priceList.id,
prices: priceList.prices
.filter((price) =>
variantIdPriceSetIdMap.has(price.variants?.[0]?.id)
)
.map((price) => {
return {
price_set_id: variantIdPriceSetIdMap.get(
price.variants?.[0]?.id
)!,
currency_code: price.currency_code,
amount: price.amount,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
}
}),
}
})
)
)
}
if (priceListsToCreate.length) {
promises.push(
pricingModuleService.createPriceLists(
priceListsToCreate.map(
({ name: title, prices, customer_groups, ...priceList }) => {
const createData: PricingTypes.CreatePriceListDTO = {
...priceList,
starts_at: priceList.starts_at?.toISOString(),
ends_at: priceList.ends_at?.toISOString(),
title,
}
if (customer_groups?.length) {
createData.rules = {
customer_group_id: customer_groups.map(({ id }) => id),
}
}
if (prices?.length) {
createData.prices = prices.map((price) => {
return {
price_set_id: variantIdPriceSetIdMap.get(
price.variants?.[0]?.id
)!,
currency_code: price.currency_code,
amount: price.amount,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
}
})
}
return createData
}
)
)
)
}
await promiseAll(promises)
offset += corePriceLists.length
Logger.info(`Processed ${offset} of ${totalCount}`)
}
}
const ensureCurrencies = async (container: AwilixContainer) => {
const currenciesService: CurrencyService =
container.resolve("currencyService")
const pricingModuleService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
const [coreCurrencies, totalCurrencies] =
await currenciesService.listAndCount({}, {})
const moduleCurrencies = await pricingModuleService.listCurrencies(
{},
{ take: 100000 }
)
const moduleCurrenciesSet = new Set(moduleCurrencies.map(({ code }) => code))
const currenciesToCreate = coreCurrencies
.filter(({ code }) => {
return !moduleCurrenciesSet.has(code)
})
.map(({ includes_tax, ...currency }) => currency)
await pricingModuleService.createCurrencies(currenciesToCreate)
}
const migrate = async function ({ directory }) {
const app = express()
const { container } = await loaders({
directory,
expressApp: app,
isTest: false,
})
Logger.info("-----------------------------------------------")
Logger.info("------------- Creating currencies -------------")
Logger.info("-----------------------------------------------")
await ensureCurrencies(container)
Logger.info("-----------------------------------------------")
Logger.info("--------- Creating default rule types ---------")
Logger.info("-----------------------------------------------")
await createDefaultRuleTypes(container)
Logger.info("-----------------------------------------------")
Logger.info("---------- Migrating Variant Prices -----------")
Logger.info("-----------------------------------------------")
await migrateProductVariantPricing(container)
Logger.info("-----------------------------------------------")
Logger.info("----------- Migrating Price Lists -------------")
Logger.info("-----------------------------------------------")
return await migratePriceLists(container)
}
migrate({ directory: process.cwd() })
.then(() => {
Logger.info("Migrated price lists")
process.exit(0)
})
.catch((error) => {
console.warn(error)
Logger.info("Failed to migrate price lists")
process.exit(1)
})
@@ -1,140 +0,0 @@
import { IPricingModuleService, MedusaContainer } from "@medusajs/types"
import {
FlagRouter,
MedusaError,
MedusaV2Flag,
promiseAll,
} from "@medusajs/utils"
import dotenv from "dotenv"
import express from "express"
import { EntityManager } from "typeorm"
import loaders from "../loaders"
import loadMedusaApp from "../loaders/medusa-app"
import { ProductVariant } from "../models"
import { ProductVariantService } from "../services"
import { createDefaultRuleTypes } from "./create-default-rule-types"
dotenv.config()
const BATCH_SIZE = 100
const migrateProductVariant = async (
variant: ProductVariant,
{
container,
}: { container: MedusaContainer; transactionManager: EntityManager }
) => {
const pricingService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
const configModule = await container.resolve("configModule")
const { link } = await loadMedusaApp(
{ configModule, container },
{ registerInContainer: false }
)
if (!link) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Can't migrate money_amounts: Pricing module is not configured correctly"
)
}
const priceSet = await pricingService.create({
rules: [{ rule_attribute: "region_id" }],
prices: variant.prices.map((price) => ({
rules: {
...(price.region_id ? { region_id: price.region_id } : {}),
},
currency_code: price.currency_code,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
})),
})
await link.create({
productService: {
variant_id: variant.id,
},
pricingService: {
price_set_id: priceSet.id,
},
})
}
const processBatch = async (
variants: ProductVariant[],
container: MedusaContainer
) => {
const manager = container.resolve("manager")
return await manager.transaction(async (transactionManager) => {
await promiseAll(
variants.map(async (variant) => {
await migrateProductVariant(variant, {
container,
transactionManager,
})
})
)
})
}
const migrate = async function ({ directory }) {
const app = express()
const { container } = await loaders({
directory,
expressApp: app,
isTest: false,
})
const variantService: ProductVariantService = await container.resolve(
"productVariantService"
)
const featureFlagRouter: FlagRouter = await container.resolve(
"featureFlagRouter"
)
if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Pricing module not enabled"
)
}
await createDefaultRuleTypes(container)
const [variants, totalCount] = await variantService.listAndCount(
{},
{ take: BATCH_SIZE, order: { id: "ASC" }, relations: ["prices"] }
)
await processBatch(variants, container)
let processedCount = variants.length
console.log(`Processed ${processedCount} of ${totalCount}`)
while (processedCount < totalCount) {
const nextBatch = await variantService.list(
{},
{
skip: processedCount,
take: BATCH_SIZE,
order: { id: "ASC" },
relations: ["prices"],
}
)
await processBatch(nextBatch, container)
processedCount += nextBatch.length
console.log(`Processed ${processedCount} of ${totalCount}`)
}
console.log("Done")
process.exit(0)
}
migrate({ directory: process.cwd() })
@@ -0,0 +1,31 @@
import { AwilixContainer } from "awilix"
import { IPricingModuleService } from "@medusajs/types"
export const createDefaultRuleTypes = async (container: AwilixContainer) => {
const pricingModuleService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
const existing = await pricingModuleService.listRuleTypes(
{ rule_attribute: ["region_id", "customer_group_id"] },
{ take: 2 }
)
if (existing.length === 2) {
return
}
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" },
])
}
}
@@ -0,0 +1,83 @@
import { IPricingModuleService, MedusaContainer } from "@medusajs/types"
import { MedusaError, promiseAll } from "@medusajs/utils"
import { ProductVariantService } from "../../services"
import dotenv from "dotenv"
dotenv.config()
const BATCH_SIZE = 100
export const migrateProductVariantPricing = async function (
container: MedusaContainer
) {
const variantService: ProductVariantService = await container.resolve(
"productVariantService"
)
const pricingService: IPricingModuleService = container.resolve(
"pricingModuleService"
)
const link = await container.resolve("remoteLink")
if (!link) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Can't migrate money_amounts: Pricing module is not configured correctly"
)
}
const [_, totalCount] = await variantService.listAndCount(
{},
{ take: BATCH_SIZE, order: { id: "ASC" }, relations: ["prices"] }
)
let processedCount = 0
while (processedCount < totalCount) {
const [variants] = await variantService.listAndCount(
{},
{
skip: processedCount,
take: BATCH_SIZE,
order: { id: "ASC" },
relations: ["prices"],
}
)
const links: any[] = []
await promiseAll(
variants.map(async (variant) => {
const priceSet = await pricingService.create({
rules: [{ rule_attribute: "region_id" }],
prices:
variant?.prices
?.filter(({ price_list_id }) => !price_list_id)
.map((price) => ({
rules: {
...(price.region_id ? { region_id: price.region_id } : {}),
},
currency_code: price.currency_code,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
})) ?? [],
})
links.push({
productService: {
variant_id: variant.id,
},
pricingService: {
price_set_id: priceSet.id,
},
})
})
)
await link.create(links)
processedCount += variants.length
console.log(`Processed ${processedCount} of ${totalCount}`)
}
}