diff --git a/.changeset/angry-ghosts-taste.md b/.changeset/angry-ghosts-taste.md new file mode 100644 index 0000000000..4070019477 --- /dev/null +++ b/.changeset/angry-ghosts-taste.md @@ -0,0 +1,7 @@ +--- +"@medusajs/workflows": patch +"@medusajs/pricing": patch +"@medusajs/medusa": patch +--- + +fix(workflows, pricing, medusa): update region variants fix + pricing module migration scripts diff --git a/packages/medusa/src/scripts/create-default-rule-types.ts b/packages/medusa/src/scripts/create-default-rule-types.ts index e75b96b572..b5d2612c36 100644 --- a/packages/medusa/src/scripts/create-default-rule-types.ts +++ b/packages/medusa/src/scripts/create-default-rule-types.ts @@ -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({ diff --git a/packages/medusa/src/scripts/migrate-price-lists.ts b/packages/medusa/src/scripts/migrate-price-lists.ts deleted file mode 100644 index aaa2cc6834..0000000000 --- a/packages/medusa/src/scripts/migrate-price-lists.ts +++ /dev/null @@ -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( - 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") - }) diff --git a/packages/medusa/src/scripts/migrate-to-pricing-module.ts b/packages/medusa/src/scripts/migrate-to-pricing-module.ts new file mode 100644 index 0000000000..46e503b2b3 --- /dev/null +++ b/packages/medusa/src/scripts/migrate-to-pricing-module.ts @@ -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( + 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( + variantPriceSets.map((vps) => [vps.variant_id, vps.price_set_id]) + ) + + const promises: Promise[] = [] + + 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) + }) diff --git a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts b/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts deleted file mode 100644 index 48f888ba41..0000000000 --- a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts +++ /dev/null @@ -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() }) diff --git a/packages/medusa/src/scripts/utils/create-default-rule-types.ts b/packages/medusa/src/scripts/utils/create-default-rule-types.ts new file mode 100644 index 0000000000..6403b98d03 --- /dev/null +++ b/packages/medusa/src/scripts/utils/create-default-rule-types.ts @@ -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" }, + ]) + } +} diff --git a/packages/medusa/src/scripts/utils/migrate-money-amounts-to-pricing-module.ts b/packages/medusa/src/scripts/utils/migrate-money-amounts-to-pricing-module.ts new file mode 100644 index 0000000000..4a1e26076a --- /dev/null +++ b/packages/medusa/src/scripts/utils/migrate-money-amounts-to-pricing-module.ts @@ -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}`) + } +} diff --git a/packages/pricing/src/migrations/Migration20231101232834.ts b/packages/pricing/src/migrations/Migration20231101232834.ts index 8056911665..a50aad2cd9 100644 --- a/packages/pricing/src/migrations/Migration20231101232834.ts +++ b/packages/pricing/src/migrations/Migration20231101232834.ts @@ -14,13 +14,10 @@ export class Migration20231101232834 extends Migration { ) this.addSql( - `ALTER TABLE price_list ADD COLUMN IF NOT EXISTS number_rules integer not null default 0` + `ALTER TABLE price_list + ADD COLUMN IF NOT EXISTS number_rules integer not null default 0` ) - - this.addSql( - 'alter table "price_list" add column if not exists "title" text not null, add column if not exists "description" text not null, add column if not exists "type" text check ("type" in (\'sale\', \'override\')) not null default \'sale\';' - ) - + this.addSql( 'alter table "price_set_money_amount" drop constraint "price_set_money_amount_price_list_id_foreign";' ) @@ -30,9 +27,19 @@ export class Migration20231101232834 extends Migration { ) this.addSql( - 'alter table "price_list" add column if not exists "title" text not null, add column if not exists "description" text not null, add column if not exists "type" text check ("type" in (\'sale\', \'override\')) not null default \'sale\', add column if not exists "created_at" timestamptz not null default now(), add column if not exists "updated_at" timestamptz not null default now(), add column if not exists "deleted_at" timestamptz null;' + 'alter table "price_list" add column if not exists "title" text, add column if not exists "name" text, add column if not exists "description" text not null, add column if not exists "type" text check ("type" in (\'sale\', \'override\')) not null default \'sale\', add column if not exists "created_at" timestamptz not null default now(), add column if not exists "updated_at" timestamptz not null default now(), add column if not exists "deleted_at" timestamptz null;' ) + this.addSql(` + UPDATE "price_list" + SET title = name + `) + + this.addSql(`alter table "price_list" + alter column "title" set not null `) + + this.addSql('alter table "price_list" drop column if exists "name";') + this.addSql( 'create index if not exists "IDX_price_list_deleted_at" on "price_list" ("deleted_at");' ) diff --git a/packages/workflows/src/handlers/product/upsert-variant-prices.ts b/packages/workflows/src/handlers/product/upsert-variant-prices.ts index 0575d5aedd..441ed9d3e1 100644 --- a/packages/workflows/src/handlers/product/upsert-variant-prices.ts +++ b/packages/workflows/src/handlers/product/upsert-variant-prices.ts @@ -78,29 +78,34 @@ export async function upsertVariantPrices({ ) for (const price of prices) { + const region = price.region_id && regionsMap.get(price.region_id) + let region_currency_code: string | undefined + let region_rules: Record | undefined + + if (region) { + region_currency_code = region.currency_code + region_rules = { + region_id: region.id, + } + } + if (price.id) { - moneyAmountsToUpdate.push({ + const priceToUpdate = { id: price.id, min_quantity: price.min_quantity, max_quantity: price.max_quantity, amount: price.amount, - currency_code: price.currency_code, - }) + currency_code: region_currency_code ?? price.currency_code, + } + + moneyAmountsToUpdate.push(priceToUpdate) } else { - const region = price.region_id && regionsMap.get(price.region_id) const variantPrice: PricingTypes.CreatePricesDTO = { min_quantity: price.min_quantity, max_quantity: price.max_quantity, amount: price.amount, - currency_code: price.currency_code, - rules: {}, - } - - if (region) { - variantPrice.currency_code = region.currency_code - variantPrice.rules = { - region_id: region.id, - } + currency_code: region_currency_code ?? price.currency_code, + rules: region_rules ?? {}, } delete price.region_id