feat(medusa): integrate pricing module to core (#5304)

* add pricing integraiton feature flag

* init

* first endpoint

* cleanup

* remove console.logs

* refactor to util and implement across endpoints

* add changeset

* rename variables

* remove mistype

* feat(medusa): move price module integration to pricing service (#5322)

* initial changes

* chore: make product service always internal for pricing module

* add notes

---------

Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>

* nit

* cleanup

* update to object querying

* update cart integration test

* remove uppercase currency_code

* nit

* Feat/admin product pricing module reads (#5354)

* initial changes to list prices for admin

* working price module implementation of list prices

* nit

* variant pricing

* redo integration test changes

* cleanup

* cleanup

* fix unit tests

* [wip] Core <> Pricing - price updates  (#5364)

* chore: update medusa-app

* wip

* get links and modules working with migration

* wip

* chore: make test pass

* Feat/rule type utils (#5371)

* initial rule type utils

* update migration script

* chore: cleanup

* ensure prices are always decorated

* chore: use seed instead

* chore: fix oas conflict

* region id add to admin price read!

---------

Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
Co-authored-by: Philip Korsholm <philip.korsholm@hotmail.com>

* pr feedback

* create remoteQueryFunction type

* fix merge

* fix loaders issue

* Feat(medusa, types, pricing): pricing module migration script (#5409)

* add migration script for money amounts in pricing module

* add changeset

* rename file

* cleanup imports

* update changeset

* add check for pricing module and ff

* feat(medusa,workflows,types): update prices on product and variant update (#5412)

* wip

* chore: update product prices through workflow

* chore: cleanup

* chore: update product handler updates prices for variants

* chore: handle reverts

* chore: address pr comments

* chore: scope workflow handlers to flag handlers

* chore: update return

* chore: update db url

* chore: remove migration

* chore: increase jest timeout

* Feat(medusa): update migration and initDb to run link-migrations (#5437)

* initial

* loader update

* more progress on loaders

* update integration tests and remote-query loader

* remove helper

* migrate isolated modules

* fix test

* fix integration test

* update with pr feedback

* unregister medusa-app

* re-register medusaApp

* fix featureflag

* set timeout

* set timeout

* conditionally run link-module migrations

* pr feedback 1

* add driver options for db

* throw if link is not defined in migration script

* pass config module directly

* include container in migrate command

* chore: increase timeout

* rm redis from api integration tests to test

* chore: temporarily skip tests

* chore: undo skips + add timeout for workflow tests

* chore: increase timeout for order edits

* re-add redis

* include final resolution

* add sharedcontainer to medusaapp loader

* chore: move migration under run command

* try removing redis_url from api tests

* chore: cleanup server on process exit

* chore: clear container on exit

* chore: adjustments

* chore: remove consoles

* chore: close express app on finish

* chore: destroy pg connection on shutdown

* chore: skip

* chore: unskip test

* chore: cleanup container pg connection

* chore: skip

---------

Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
Philip Korsholm
2023-10-30 14:42:17 +01:00
committed by GitHub
parent b69f182571
commit 148f537b47
84 changed files with 2702 additions and 284 deletions

View File

@@ -1,2 +1,3 @@
export * from "./create-products"
export * as UpdateProductVariants from "./update-product-variants"
export * from "./update-products"

View File

@@ -0,0 +1,108 @@
import {
TransactionStepsDefinition,
WorkflowManager,
} from "@medusajs/orchestration"
import { InputAlias, Workflows } from "../../definitions"
import { exportWorkflow, pipe } from "../../helper"
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
import { ProductHandlers } from "../../handlers"
export enum UpdateProductVariantsActions {
prepare = "prepare",
updateProductVariants = "updateProductVariants",
revertProductVariantsUpdate = "revertProductVariantsUpdate",
upsertPrices = "upsertPrices",
}
export const workflowSteps: TransactionStepsDefinition = {
next: {
action: UpdateProductVariantsActions.prepare,
noCompensation: true,
next: {
action: UpdateProductVariantsActions.updateProductVariants,
noCompensation: true,
next: [
{
action: UpdateProductVariantsActions.upsertPrices,
},
],
},
},
}
const handlers = new Map([
[
UpdateProductVariantsActions.prepare,
{
invoke: pipe(
{
merge: true,
inputAlias: InputAlias.ProductVariantsUpdateInputData,
invoke: {
from: InputAlias.ProductVariantsUpdateInputData,
},
},
ProductHandlers.updateProductVariantsPrepareData
),
},
],
[
UpdateProductVariantsActions.updateProductVariants,
{
invoke: pipe(
{
merge: true,
invoke: {
from: UpdateProductVariantsActions.prepare,
},
},
ProductHandlers.updateProductVariants
),
},
],
[
UpdateProductVariantsActions.upsertPrices,
{
invoke: pipe(
{
merge: true,
invoke: [
{
from: UpdateProductVariantsActions.prepare,
},
],
},
ProductHandlers.upsertVariantPrices
),
compensate: pipe(
{
merge: true,
invoke: [
{
from: UpdateProductVariantsActions.prepare,
},
{
from: UpdateProductVariantsActions.upsertPrices,
},
],
},
ProductHandlers.revertVariantPrices
),
},
],
])
WorkflowManager.register(
Workflows.UpdateProductVariants,
workflowSteps,
handlers
)
export const updateProductVariants = exportWorkflow<
WorkflowTypes.ProductWorkflow.UpdateProductVariantsWorkflowInputDTO,
ProductTypes.ProductVariantDTO[]
>(
Workflows.UpdateProductVariants,
UpdateProductVariantsActions.updateProductVariants
)

View File

@@ -1,16 +1,17 @@
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
import { InputAlias, Workflows } from "../../definitions"
import {
TransactionStepsDefinition,
WorkflowManager,
} from "@medusajs/orchestration"
import { exportWorkflow, pipe } from "../../helper"
import { CreateProductsActions } from "./create-products"
import { InputAlias, Workflows } from "../../definitions"
import { InventoryHandlers, ProductHandlers } from "../../handlers"
import * as MiddlewareHandlers from "../../handlers/middlewares"
import { detachSalesChannelFromProducts } from "../../handlers/product"
import { exportWorkflow, pipe } from "../../helper"
import { CreateProductsActions } from "./create-products"
import { prepareCreateInventoryItems } from "./prepare-create-inventory-items"
import { UpdateProductVariantsActions } from "./update-product-variants"
export enum UpdateProductsActions {
prepare = "prepare",
@@ -32,6 +33,10 @@ export const updateProductsWorkflowSteps: TransactionStepsDefinition = {
next: {
action: UpdateProductsActions.updateProducts,
next: [
{
action: UpdateProductVariantsActions.upsertPrices,
saveResponse: false,
},
{
action: UpdateProductsActions.attachSalesChannels,
saveResponse: false,
@@ -59,7 +64,7 @@ export const updateProductsWorkflowSteps: TransactionStepsDefinition = {
},
}
const handlers = new Map([
const handlers = new Map<string, any>([
[
UpdateProductsActions.prepare,
{
@@ -350,6 +355,37 @@ const handlers = new Map([
),
},
],
[
UpdateProductVariantsActions.upsertPrices,
{
invoke: pipe(
{
merge: true,
invoke: [
{
from: InputAlias.ProductsInputData,
alias: ProductHandlers.updateProducts.aliases.products,
},
{
from: UpdateProductsActions.prepare,
},
],
},
ProductHandlers.upsertVariantPrices
),
compensate: pipe(
{
merge: true,
invoke: [
{
from: UpdateProductVariantsActions.upsertPrices,
},
],
},
ProductHandlers.revertVariantPrices
),
},
],
])
WorkflowManager.register(

View File

@@ -3,6 +3,9 @@ export enum Workflows {
CreateProducts = "create-products",
UpdateProducts = "update-products",
// Product Variant workflows
UpdateProductVariants = "update-product-variants",
// Cart workflows
CreateCart = "create-cart",
@@ -14,6 +17,9 @@ export enum InputAlias {
ProductsInputData = "productsInputData",
RemovedProducts = "removedProducts",
ProductVariants = "productVariants",
ProductVariantsUpdateInputData = "productVariantsUpdateInputData",
InventoryItems = "inventoryItems",
RemovedInventoryItems = "removedInventoryItems",

View File

@@ -1,8 +1,8 @@
import { ProductTypes, SalesChannelTypes, WorkflowTypes } from "@medusajs/types"
import {
FeatureFlagUtils,
kebabCase,
ShippingProfileUtils,
kebabCase,
} from "@medusajs/utils"
import { WorkflowArguments } from "../../helper"

View File

@@ -1,12 +1,16 @@
export * from "./create-products-prepare-data"
export * from "./create-products"
export * from "./detach-sales-channel-from-products"
export * from "./attach-sales-channel-to-products"
export * from "./detach-shipping-profile-from-products"
export * from "./remove-products"
export * from "./attach-shipping-profile-to-products"
export * from "./create-products"
export * from "./create-products-prepare-data"
export * from "./detach-sales-channel-from-products"
export * from "./detach-shipping-profile-from-products"
export * from "./list-products"
export * from "./remove-products"
export * from "./revert-update-products"
export * from "./revert-variant-prices"
export * from "./update-product-variants"
export * from "./update-product-variants-prepare-data"
export * from "./update-products"
export * from "./update-products-prepare-data"
export * from "./revert-update-products"
export * from "./update-products-variants-prices"
export * from "./upsert-variant-prices"

View File

@@ -0,0 +1,48 @@
import { PricingTypes } from "@medusajs/types"
import { WorkflowArguments } from "../../helper"
type HandlerInput = {
createdLinks: Record<any, any>[]
originalMoneyAmounts: PricingTypes.MoneyAmountDTO[]
createdPriceSets: PricingTypes.PriceSetDTO[]
}
export async function revertVariantPrices({
container,
context,
data,
}: WorkflowArguments<HandlerInput>): Promise<void> {
const {
createdLinks = [],
originalMoneyAmounts = [],
createdPriceSets = [],
} = data
const featureFlagRouter = container.resolve("featureFlagRouter")
const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled(
"isolate_pricing_domain"
)
if (!isPricingDomainEnabled) {
return
}
const pricingModuleService = container.resolve("pricingModuleService")
const remoteLink = container.resolve("remoteLink")
await remoteLink.remove(createdLinks)
if (originalMoneyAmounts.length) {
await pricingModuleService.updateMoneyAmounts(originalMoneyAmounts)
}
if (createdPriceSets.length) {
await pricingModuleService.delete({
id: createdPriceSets.map((cps) => cps.id),
})
}
}
revertVariantPrices.aliases = {
productVariantsPrices: "productVariantsPrices",
}

View File

@@ -0,0 +1,95 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductTypes, ProductWorkflow, WorkflowTypes } from "@medusajs/types"
import { WorkflowArguments } from "../../helper"
type VariantPrice = {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}
export type UpdateProductVariantsPreparedData = {
productVariants: ProductWorkflow.UpdateProductVariantsInputDTO[]
variantPricesMap: Map<string, VariantPrice[]>
productVariantsMap: Map<
string,
ProductWorkflow.UpdateProductVariantsInputDTO[]
>
}
export async function updateProductVariantsPrepareData({
container,
context,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.UpdateProductVariantsWorkflowInputDTO>): Promise<UpdateProductVariantsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled(
"isolate_pricing_domain"
)
let productVariants: ProductWorkflow.UpdateProductVariantsInputDTO[] =
data.productVariants || []
const variantsDataMap = new Map<
string,
ProductWorkflow.UpdateProductVariantsInputDTO
>(
productVariants.map((productVariantData) => [
productVariantData.id,
productVariantData,
])
)
const variantIds = productVariants.map((pv) => pv.id) as string[]
const productVariantsMap = new Map<
string,
ProductWorkflow.UpdateProductVariantsInputDTO[]
>()
const variantPricesMap = new Map<string, VariantPrice[]>()
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
const variantsWithProductIds = await productModuleService.listVariants(
{
id: variantIds,
},
{
select: ["id", "product_id"],
}
)
for (const variantWithProductID of variantsWithProductIds) {
const variantData = variantsDataMap.get(variantWithProductID.id)
if (!variantData) {
continue
}
variantPricesMap.set(variantWithProductID.id, variantData.prices || [])
if (isPricingDomainEnabled) {
delete variantData.prices
}
const variantsData: ProductWorkflow.UpdateProductVariantsInputDTO[] =
productVariantsMap.get(variantWithProductID.product_id) || []
if (variantData) {
variantsData.push(variantData)
}
productVariantsMap.set(variantWithProductID.product_id, variantsData)
}
return {
productVariants,
variantPricesMap,
productVariantsMap,
}
}
updateProductVariantsPrepareData.aliases = {
payload: "payload",
}

View File

@@ -0,0 +1,39 @@
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { ProductTypes } from "@medusajs/types"
import { WorkflowArguments } from "../../helper"
type HandlerInput = {
productVariantsMap: Map<string, ProductTypes.UpdateProductVariantDTO[]>
}
export async function updateProductVariants({
container,
data,
}: WorkflowArguments<HandlerInput>): Promise<
ProductTypes.UpdateProductVariantDTO[]
> {
const { productVariantsMap } = data
const productsVariants: ProductTypes.UpdateProductVariantDTO[] = []
const updateProductsData: ProductTypes.UpdateProductDTO[] = []
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
for (const [productId, variantsData = []] of productVariantsMap) {
updateProductsData.push({
id: productId,
variants: variantsData,
})
productsVariants.push(...variantsData)
}
if (updateProductsData.length) {
await productModuleService.update(updateProductsData)
}
return productsVariants
}
updateProductVariants.aliases = {
payload: "payload",
}

View File

@@ -6,10 +6,19 @@ type ProductWithSalesChannelsDTO = ProductDTO & {
sales_channels?: SalesChannelDTO[]
}
type VariantPrice = {
region_id?: string
currency_code?: string
amount: number
min_quantity?: number
max_quantity?: number
}
export type UpdateProductsPreparedData = {
originalProducts: ProductWithSalesChannelsDTO[]
productHandleAddedChannelsMap: Map<string, string[]>
productHandleRemovedChannelsMap: Map<string, string[]>
variantPricesMap: Map<string, VariantPrice[]>
}
export async function updateProductsPrepareData({
@@ -17,6 +26,12 @@ export async function updateProductsPrepareData({
context,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.UpdateProductsWorkflowInputDTO>): Promise<UpdateProductsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const isPricingDomainEnabled = featureFlagRouter.isFeatureEnabled(
"isolate_pricing_domain"
)
const variantPricesMap = new Map<string, VariantPrice[]>()
const ids = data.products.map((product) => product.id)
const productHandleAddedChannelsMap = new Map<string, string[]>()
@@ -65,6 +80,16 @@ export async function updateProductsPrepareData({
})
}
for (const variantInput of productInput.variants || []) {
if (variantInput.id) {
variantPricesMap.set(variantInput.id, variantInput.prices || [])
}
if (isPricingDomainEnabled) {
delete variantInput.prices
}
}
productHandleAddedChannelsMap.set(currentProduct.handle!, addedChannels)
productHandleRemovedChannelsMap.set(currentProduct.handle!, removedChannels)
})
@@ -73,6 +98,7 @@ export async function updateProductsPrepareData({
originalProducts: products,
productHandleAddedChannelsMap,
productHandleRemovedChannelsMap,
variantPricesMap,
}
}

View File

@@ -1,4 +1,5 @@
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { WorkflowArguments } from "../../helper"
@@ -26,9 +27,12 @@ export async function updateProductsVariantsPrices({
data.productsHandleVariantsIndexPricesMap
const productVariantService = container.resolve("productVariantService")
const regionService = container.resolve("regionService")
const featureFlagRouter = container.resolve("featureFlagRouter")
const productVariantServiceTx = productVariantService.withTransaction(manager)
const variantIdsPricesData: any[] = []
const variantPricesMap = new Map<string, any[]>()
const productsMap = new Map<string, ProductTypes.ProductDTO>(
products.map((p) => [p.handle!, p])
)
@@ -50,10 +54,52 @@ export async function updateProductsVariantsPrices({
variantId: variant.id,
prices: item.prices,
})
variantPricesMap.set(variant.id, [])
item.prices.forEach(async (price) => {
const obj = {
amount: price.amount,
currency_code: price.currency_code,
rules: {},
}
if (price.region_id) {
const region = await regionService.retrieve(price.region_id)
obj.currency_code = region.currency_code
obj.rules = {
region_id: price.region_id,
}
}
const variantPrices = variantPricesMap.get(variant.id)
variantPrices?.push(obj)
})
})
}
await productVariantServiceTx.updateVariantPrices(variantIdsPricesData)
if (featureFlagRouter.isFeatureEnabled("isolate_pricing_domain")) {
const remoteLink = container.resolve("remoteLink")
const pricingModuleService = container.resolve("pricingModuleService")
for (let { variantId } of variantIdsPricesData) {
const priceSet = await pricingModuleService.create({
rules: [{ rule_attribute: "region_id" }],
prices: variantPricesMap.get(variantId),
})
await remoteLink.create({
productService: {
variant_id: variantId,
},
pricingService: {
price_set_id: priceSet.id,
},
})
}
} else {
await productVariantServiceTx.updateVariantPrices(variantIdsPricesData)
}
}
updateProductsVariantsPrices.aliases = {

View File

@@ -0,0 +1,168 @@
import { PricingTypes } from "@medusajs/types"
import { WorkflowArguments } from "../../helper"
type VariantPrice = {
id?: string
region_id?: string
currency_code: string
amount: number
min_quantity?: number
max_quantity?: number
rules: Record<string, string>
}
type RegionDTO = {
id: string
currency_code: string
}
type HandlerInput = {
variantPricesMap: Map<string, VariantPrice[]>
}
export async function upsertVariantPrices({
container,
context,
data,
}: WorkflowArguments<HandlerInput>) {
const { variantPricesMap } = data
const featureFlagRouter = container.resolve("featureFlagRouter")
if (!featureFlagRouter.isFeatureEnabled("isolate_pricing_domain")) {
return {
createdLinks: [],
originalMoneyAmounts: [],
createdPriceSets: [],
}
}
const pricingModuleService = container.resolve("pricingModuleService")
const regionService = container.resolve("regionService")
const remoteLink = container.resolve("remoteLink")
const remoteQuery = container.resolve("remoteQuery")
const variables = {
variant_id: [...variantPricesMap.keys()],
}
const query = {
product_variant_price_set: {
__args: variables,
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIdToPriceSetIdMap: Map<string, string> = new Map(
variantPriceSets.map((variantPriceSet) => [
variantPriceSet.variant_id,
variantPriceSet.price_set_id,
])
)
const moneyAmountsToUpdate: PricingTypes.UpdateMoneyAmountDTO[] = []
const createdPriceSets: PricingTypes.PriceSetDTO[] = []
const ruleSetPricesToAdd: PricingTypes.CreatePricesDTO[] = []
const linksToCreate: any[] = []
for (const [variantId, prices = []] of variantPricesMap) {
const priceSetToCreate: PricingTypes.CreatePriceSetDTO = {
rules: [{ rule_attribute: "region_id" }],
prices: [],
}
const regionIds = prices.map((price) => price.region_id)
const regions = await regionService.list({ id: regionIds })
const regionsMap: Map<string, RegionDTO> = new Map(
regions.map((region: RegionDTO) => [region.id, region])
)
for (const price of prices) {
if (price.id) {
moneyAmountsToUpdate.push({
id: price.id,
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
currency_code: price.currency_code,
})
} 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,
}
}
delete price.region_id
if (variantIdToPriceSetIdMap.get(variantId)) {
ruleSetPricesToAdd.push(variantPrice)
} else {
priceSetToCreate.prices?.push(variantPrice)
}
}
}
let priceSetId = variantIdToPriceSetIdMap.get(variantId)
if (priceSetId) {
await pricingModuleService.addPrices({
priceSetId,
prices: ruleSetPricesToAdd,
})
} else {
const createdPriceSet = await pricingModuleService.create(
priceSetToCreate
)
priceSetId = createdPriceSet?.id
createdPriceSets.push(createdPriceSet)
}
linksToCreate.push({
productService: {
variant_id: variantId,
},
pricingService: {
price_set_id: priceSetId,
},
})
}
const createdLinks = await remoteLink.create(linksToCreate)
let originalMoneyAmounts = await pricingModuleService.listMoneyAmounts(
{
id: moneyAmountsToUpdate.map((matu) => matu.id),
},
{
select: ["id", "currency_code", "amount", "min_quantity", "max_quantity"],
}
)
if (moneyAmountsToUpdate.length) {
await pricingModuleService.updateMoneyAmounts(moneyAmountsToUpdate)
}
return {
createdLinks,
originalMoneyAmounts,
createdPriceSets,
}
}
upsertVariantPrices.aliases = {
productVariantsPrices: "productVariantsPrices",
}