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:
84 files changed
+2702
-284
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
|
||||
describe("GET /admin/products/:id", () => {
|
||||
describe("successfully gets a product", () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PricingService, ProductService } from "../../../../services"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IsString } from "class-validator"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/products/{id}/options
|
||||
@@ -91,7 +91,7 @@ export default async (req, res) => {
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
const [product] = await pricingService.setAdminProductPricing([rawProduct])
|
||||
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
||||
import { Workflows, createProducts } from "@medusajs/workflows"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
@@ -37,17 +39,15 @@ import {
|
||||
} from "./transaction/create-product-variant"
|
||||
|
||||
import { DistributedTransaction } from "@medusajs/orchestration"
|
||||
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { createProducts, Workflows } from "@medusajs/workflows"
|
||||
import { Type } from "class-transformer"
|
||||
import { EntityManager } from "typeorm"
|
||||
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||
import { ProductStatus } from "../../../../models"
|
||||
import { Logger } from "../../../../types/global"
|
||||
import { validator } from "../../../../utils"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/products
|
||||
@@ -269,7 +269,9 @@ export default async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
const [pricedProduct] = await pricingService.setProductPrices([rawProduct])
|
||||
const [pricedProduct] = await pricingService.setAdminProductPricing([
|
||||
rawProduct,
|
||||
])
|
||||
|
||||
res.json({ product: pricedProduct })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
CreateProductVariantInput,
|
||||
ProductVariantPricesCreateReq,
|
||||
} from "../../../../types/product-variant"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
@@ -9,20 +11,19 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import {
|
||||
PricingService,
|
||||
ProductService,
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
} from "../../../../services"
|
||||
import {
|
||||
CreateProductVariantInput,
|
||||
ProductVariantPricesCreateReq,
|
||||
} from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { Type } from "class-transformer"
|
||||
import { createVariantsTransaction } from "./transaction/create-product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/products/{id}/variants
|
||||
@@ -152,7 +153,7 @@ export default async (req, res) => {
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
const [product] = await pricingService.setAdminProductPricing([rawProduct])
|
||||
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default async (req, res) => {
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
|
||||
const [product] = await pricingService.setProductPrices([data])
|
||||
const [product] = await pricingService.setAdminProductPricing([data])
|
||||
|
||||
res.json({
|
||||
variant_id,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PricingService, ProductService } from "../../../../services"
|
||||
|
||||
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { defaultAdminProductRemoteQueryObject } from "./index"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { defaultAdminProductRemoteQueryObject } from "./index"
|
||||
|
||||
/**
|
||||
* @oas [get] /admin/products/{id}
|
||||
@@ -80,8 +81,8 @@ export default async (req, res) => {
|
||||
|
||||
const product = rawProduct
|
||||
|
||||
if (!shouldSetPricing) {
|
||||
await pricingService.setProductPrices([product])
|
||||
if (shouldSetPricing) {
|
||||
await pricingService.setAdminProductPricing([product])
|
||||
}
|
||||
|
||||
res.json({ product })
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
|
||||
import { FilterableProductProps } from "../../../../types/product"
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { PricedProduct } from "../../../../types/pricing"
|
||||
import { Product } from "../../../../models"
|
||||
import { Type } from "class-transformer"
|
||||
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { defaultAdminProductRemoteQueryObject } from "./index"
|
||||
|
||||
/**
|
||||
@@ -275,7 +275,7 @@ export default async (req, res) => {
|
||||
)
|
||||
|
||||
if (shouldSetPricing) {
|
||||
products = await pricingService.setProductPrices(rawProducts)
|
||||
products = await pricingService.setAdminProductPricing(rawProducts)
|
||||
}
|
||||
|
||||
// We only set availability if variants are requested
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
|
||||
import { IsString } from "class-validator"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IsString } from "class-validator"
|
||||
import { PricingService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/products/{id}/metadata
|
||||
@@ -96,7 +96,7 @@ export default async (req, res) => {
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
const [product] = await pricingService.setAdminProductPricing([rawProduct])
|
||||
|
||||
res.status(200).json({ product })
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PricingService, ProductService } from "../../../../services"
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
|
||||
import { IsString } from "class-validator"
|
||||
import { PricingService, ProductService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IsString } from "class-validator"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/products/{id}/options/{option_id}
|
||||
@@ -92,7 +92,7 @@ export default async (req, res) => {
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
const [product] = await pricingService.setAdminProductPricing([rawProduct])
|
||||
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -137,7 +137,6 @@ export default async (req, res) => {
|
||||
req.scope.resolve("inventoryService")
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
const productModuleService = req.scope.resolve("productModuleService")
|
||||
|
||||
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
@@ -211,6 +210,7 @@ export default async (req, res) => {
|
||||
prices: variant.prices || [],
|
||||
})
|
||||
variantsToCreate.push(variant)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ export default async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
const [product] = await pricingService.setAdminProductPricing([rawProduct])
|
||||
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { WorkflowTypes } from "@medusajs/types"
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { UpdateProductVariants } from "@medusajs/workflows"
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
@@ -7,15 +11,15 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
||||
import IsolatePricingDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-pricing-domain"
|
||||
import {
|
||||
PricingService,
|
||||
ProductService,
|
||||
ProductVariantService,
|
||||
} from "../../../../services"
|
||||
|
||||
import { Type } from "class-transformer"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { ProductVariantPricesUpdateReq } from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
@@ -106,6 +110,13 @@ import { validator } from "../../../../utils/validator"
|
||||
export default async (req, res) => {
|
||||
const { id, variant_id } = req.params
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
const validated = await validator(
|
||||
AdminPostProductsProductVariantsVariantReq,
|
||||
req.body
|
||||
@@ -113,21 +124,36 @@ export default async (req, res) => {
|
||||
|
||||
const validatedQueryParams = await validator(PriceSelectionParams, req.query)
|
||||
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
if (featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key)) {
|
||||
const updateVariantsWorkflow = UpdateProductVariants.updateProductVariants(
|
||||
req.scope
|
||||
)
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await productVariantService
|
||||
.withTransaction(transactionManager)
|
||||
.update(variant_id, {
|
||||
product_id: id,
|
||||
...validated,
|
||||
})
|
||||
})
|
||||
const input = {
|
||||
productVariants: [
|
||||
{
|
||||
id: variant_id,
|
||||
...validated,
|
||||
},
|
||||
] as WorkflowTypes.ProductWorkflow.UpdateProductVariantsInputDTO[],
|
||||
}
|
||||
|
||||
await updateVariantsWorkflow.run({
|
||||
input,
|
||||
context: {
|
||||
manager,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await productVariantService
|
||||
.withTransaction(transactionManager)
|
||||
.update(variant_id, {
|
||||
product_id: id,
|
||||
...validated,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const rawProduct = await productService.retrieve(id, {
|
||||
select: defaultAdminProductFields,
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async (req, res) => {
|
||||
req.retrieveConfig
|
||||
)
|
||||
|
||||
const [variant] = await pricingService.setVariantPrices([rawVariant])
|
||||
const [variant] = await pricingService.setAdminVariantPricing([rawVariant])
|
||||
|
||||
res.status(200).json({ variant })
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export default async (req, res) => {
|
||||
currencyCode = region.currency_code
|
||||
}
|
||||
|
||||
let variants = await pricingService.setVariantPrices(rawVariants, {
|
||||
let variants = await pricingService.setAdminVariantPricing(rawVariants, {
|
||||
cart_id: req.validatedQuery.cart_id,
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
CartService,
|
||||
PricingService,
|
||||
@@ -5,13 +6,12 @@ import {
|
||||
ProductVariantInventoryService,
|
||||
RegionService,
|
||||
} from "../../../../services"
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { cleanResponseData } from "../../../../utils"
|
||||
import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { defaultStoreProductRemoteQueryObject } from "./index"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* @oas [get] /store/products/{id}
|
||||
|
||||
@@ -17,14 +17,14 @@ import { Transform, Type } from "class-transformer"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import PricingService from "../../../../services/pricing"
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
||||
import { defaultStoreCategoryScope } from "../product-categories"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||
import { defaultStoreProductRemoteQueryObject } from "./index"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
|
||||
/**
|
||||
* @oas [get] /store/products
|
||||
|
||||
@@ -7,9 +7,7 @@ import {
|
||||
} from "../../../../services"
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { FindParams } from "../../../../types/common"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { defaultStoreVariantRelations } from "."
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
@@ -81,10 +79,11 @@ export default async (req, res) => {
|
||||
req.scope.resolve("productVariantInventoryService")
|
||||
const cartService: CartService = req.scope.resolve("cartService")
|
||||
const regionService: RegionService = req.scope.resolve("regionService")
|
||||
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
|
||||
const customer_id = req.user?.customer_id
|
||||
|
||||
const rawVariant = await variantService.retrieve(id, req.retrieveConfig)
|
||||
const variant = await variantService.retrieve(id, req.retrieveConfig)
|
||||
|
||||
let sales_channel_id = validated.sales_channel_id
|
||||
if (req.publishableApiKeyScopes?.sales_channel_ids.length === 1) {
|
||||
@@ -104,19 +103,27 @@ export default async (req, res) => {
|
||||
currencyCode = region.currency_code
|
||||
}
|
||||
|
||||
const variantRes = await pricingService.setVariantPrices([rawVariant], {
|
||||
cart_id: validated.cart_id,
|
||||
customer_id: customer_id,
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
const decoratePromises: Promise<any>[] = []
|
||||
|
||||
const [variant] = await productVariantInventoryService.setVariantAvailability(
|
||||
variantRes,
|
||||
sales_channel_id
|
||||
decoratePromises.push(
|
||||
(await pricingService.setVariantPrices([variant], {
|
||||
cart_id: validated.cart_id,
|
||||
customer_id: customer_id,
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
include_discount_prices: true,
|
||||
})) as any
|
||||
)
|
||||
|
||||
decoratePromises.push(
|
||||
(await productVariantInventoryService.setVariantAvailability(
|
||||
[variant],
|
||||
sales_channel_id
|
||||
)) as any
|
||||
)
|
||||
|
||||
await Promise.all(decoratePromises)
|
||||
|
||||
res.json({ variant })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { IsInt, IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
CartService,
|
||||
PricingService,
|
||||
@@ -6,12 +5,13 @@ import {
|
||||
ProductVariantService,
|
||||
RegionService,
|
||||
} from "../../../../services"
|
||||
import { IsInt, IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { Type } from "class-transformer"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { NumericalComparisonOperator } from "../../../../types/common"
|
||||
import { PriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { Type } from "class-transformer"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
|
||||
/**
|
||||
* @oas [get] /store/variants
|
||||
@@ -154,11 +154,9 @@ export default async (req, res) => {
|
||||
const productVariantInventoryService: ProductVariantInventoryService =
|
||||
req.scope.resolve("productVariantInventoryService")
|
||||
const regionService: RegionService = req.scope.resolve("regionService")
|
||||
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
|
||||
const rawVariants = await variantService.list(
|
||||
filterableFields,
|
||||
req.listConfig
|
||||
)
|
||||
const variants = await variantService.list(filterableFields, req.listConfig)
|
||||
|
||||
let regionId = validated.region_id
|
||||
let currencyCode = validated.currency_code
|
||||
@@ -173,19 +171,26 @@ export default async (req, res) => {
|
||||
currencyCode = region.currency_code
|
||||
}
|
||||
|
||||
const pricedVariants = await pricingService.setVariantPrices(rawVariants, {
|
||||
cart_id: validated.cart_id,
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
customer_id: customer_id,
|
||||
include_discount_prices: true,
|
||||
})
|
||||
const decoratePromises: Promise<any>[] = []
|
||||
|
||||
const variants = await productVariantInventoryService.setVariantAvailability(
|
||||
pricedVariants,
|
||||
sales_channel_id
|
||||
decoratePromises.push(
|
||||
(await pricingService.setVariantPrices(variants, {
|
||||
cart_id: validated.cart_id,
|
||||
region_id: regionId,
|
||||
currency_code: currencyCode,
|
||||
customer_id: customer_id,
|
||||
include_discount_prices: true,
|
||||
})) as any
|
||||
)
|
||||
|
||||
decoratePromises.push(
|
||||
(await productVariantInventoryService.setVariantAvailability(
|
||||
variants,
|
||||
sales_channel_id
|
||||
)) as any
|
||||
)
|
||||
await Promise.all(decoratePromises)
|
||||
|
||||
res.json({ variants })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { asValue, createContainer } from "awilix"
|
||||
import featureFlagLoader from "../loaders/feature-flags"
|
||||
import Logger from "../loaders/logger"
|
||||
import databaseLoader from "../loaders/database"
|
||||
import configModuleLoader from "../loaders/config"
|
||||
import getMigrations, {
|
||||
getModuleSharedResources,
|
||||
revertIsolatedModulesMigration,
|
||||
runIsolatedModulesMigration,
|
||||
} from "./utils/get-migrations"
|
||||
|
||||
import { createMedusaContainer } from "@medusajs/utils"
|
||||
import configModuleLoader from "../loaders/config"
|
||||
import databaseLoader from "../loaders/database"
|
||||
import featureFlagLoader from "../loaders/feature-flags"
|
||||
import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain"
|
||||
import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain"
|
||||
import Logger from "../loaders/logger"
|
||||
import { loadMedusaApp } from "../loaders/medusa-app"
|
||||
import pgConnectionLoader from "../loaders/pg-connection"
|
||||
|
||||
const getDataSource = async (directory) => {
|
||||
const configModule = configModuleLoader(directory)
|
||||
const featureFlagRouter = featureFlagLoader(configModule)
|
||||
@@ -31,6 +37,25 @@ const getDataSource = async (directory) => {
|
||||
})
|
||||
}
|
||||
|
||||
const runLinkMigrations = async (directory) => {
|
||||
const configModule = configModuleLoader(directory)
|
||||
const container = createMedusaContainer()
|
||||
|
||||
await pgConnectionLoader({ configModule, container })
|
||||
|
||||
const { runMigrations } = await loadMedusaApp(
|
||||
{ configModule, container },
|
||||
{ registerInContainer: false }
|
||||
)
|
||||
|
||||
const options = {
|
||||
database: {
|
||||
clientUrl: configModule.projectConfig.database_url,
|
||||
},
|
||||
}
|
||||
await runMigrations(options)
|
||||
}
|
||||
|
||||
const main = async function ({ directory }) {
|
||||
const args = process.argv
|
||||
|
||||
@@ -40,19 +65,26 @@ const main = async function ({ directory }) {
|
||||
|
||||
const configModule = configModuleLoader(directory)
|
||||
const dataSource = await getDataSource(directory)
|
||||
const featureFlagRouter = featureFlagLoader(configModule)
|
||||
|
||||
if (args[0] === "run") {
|
||||
await dataSource.runMigrations()
|
||||
await dataSource.destroy()
|
||||
await runIsolatedModulesMigration(configModule)
|
||||
Logger.info("Migrations completed.")
|
||||
if (
|
||||
featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) ||
|
||||
featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key)
|
||||
) {
|
||||
await runLinkMigrations(directory)
|
||||
}
|
||||
process.exit()
|
||||
|
||||
Logger.info("Migrations completed.")
|
||||
} else if (args[0] === "revert") {
|
||||
await dataSource.undoLastMigration({ transaction: "all" })
|
||||
await dataSource.destroy()
|
||||
await revertIsolatedModulesMigration(configModule)
|
||||
Logger.info("Migrations reverted.")
|
||||
process.exit()
|
||||
} else if (args[0] === "show") {
|
||||
const unapplied = await dataSource.showMigrations()
|
||||
Logger.info(unapplied)
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import express from "express"
|
||||
import fs from "fs"
|
||||
import { sync as existsSync } from "fs-exists-cached"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
import { track } from "medusa-telemetry"
|
||||
import path from "path"
|
||||
import { DataSource, DataSourceOptions } from "typeorm"
|
||||
|
||||
import loaders from "../loaders"
|
||||
import { handleConfigError } from "../loaders/config"
|
||||
import Logger from "../loaders/logger"
|
||||
|
||||
import featureFlagLoader from "../loaders/feature-flags"
|
||||
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductService,
|
||||
@@ -23,12 +10,25 @@ import {
|
||||
StoreService,
|
||||
UserService,
|
||||
} from "../services"
|
||||
import { ConfigModule } from "../types/global"
|
||||
import { CreateProductInput } from "../types/product"
|
||||
import { CreateProductCategoryInput } from "../types/product-category"
|
||||
import getMigrations, { getModuleSharedResources } from "./utils/get-migrations"
|
||||
|
||||
import { ConfigModule } from "../types/global"
|
||||
import { CreateProductCategoryInput } from "../types/product-category"
|
||||
import { CreateProductInput } from "../types/product"
|
||||
import { IPricingModuleService } from "@medusajs/types"
|
||||
import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain"
|
||||
import Logger from "../loaders/logger"
|
||||
import PublishableApiKeyService from "../services/publishable-api-key"
|
||||
import { SalesChannel } from "../models"
|
||||
import { sync as existsSync } from "fs-exists-cached"
|
||||
import express from "express"
|
||||
import featureFlagLoader from "../loaders/feature-flags"
|
||||
import fs from "fs"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
import { handleConfigError } from "../loaders/config"
|
||||
import loaders from "../loaders"
|
||||
import path from "path"
|
||||
import { track } from "medusa-telemetry"
|
||||
|
||||
type SeedOptions = {
|
||||
directory: string
|
||||
@@ -121,6 +121,9 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) {
|
||||
const shippingProfileService: ShippingProfileService = container.resolve(
|
||||
"shippingProfileService"
|
||||
)
|
||||
const pricingModuleService: IPricingModuleService = container.resolve(
|
||||
"pricingModuleService"
|
||||
)
|
||||
/* eslint-enable */
|
||||
|
||||
await manager.transaction(async (tx) => {
|
||||
@@ -131,6 +134,7 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) {
|
||||
categories = [],
|
||||
shipping_options,
|
||||
users,
|
||||
rule_types = [],
|
||||
publishable_api_keys = [],
|
||||
} = JSON.parse(fs.readFileSync(resolvedPath, `utf-8`))
|
||||
|
||||
@@ -270,6 +274,14 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) {
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key)
|
||||
) {
|
||||
for (const ruleType of rule_types) {
|
||||
await pricingModuleService.createRuleTypes(ruleType)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
track("CLI_SEED_COMPLETED")
|
||||
|
||||
@@ -10,3 +10,5 @@ export * from "./types/routing"
|
||||
export * from "./types/global"
|
||||
export * from "./types/price-list"
|
||||
export * from "./utils"
|
||||
export * from "./joiner-config"
|
||||
export * from "./modules-config"
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FlagSettings } from "../../types/feature-flags"
|
||||
|
||||
const PricingIntegrationFeatureFlag: FlagSettings = {
|
||||
const IsolatePricingDomainFeatureFlag: FlagSettings = {
|
||||
key: "isolate_pricing_domain",
|
||||
default_val: false,
|
||||
env_key: "MEDUSA_FF_ISOLATE_PRICING_DOMAIN",
|
||||
description: "[WIP] use price module integration for pricing",
|
||||
}
|
||||
|
||||
export default PricingIntegrationFeatureFlag
|
||||
export default IsolatePricingDomainFeatureFlag
|
||||
@@ -535,7 +535,7 @@ export class RoutesLoader {
|
||||
}
|
||||
|
||||
async load() {
|
||||
performance.mark("file-base-routing-start" + this.rootDir)
|
||||
performance && performance.mark("file-base-routing-start" + this.rootDir)
|
||||
|
||||
let apiExists = true
|
||||
|
||||
@@ -563,14 +563,16 @@ export class RoutesLoader {
|
||||
await this.registerRoutes()
|
||||
}
|
||||
|
||||
performance.mark("file-base-routing-end" + this.rootDir)
|
||||
const timeSpent = performance
|
||||
.measure(
|
||||
"file-base-routing-measure" + this.rootDir,
|
||||
"file-base-routing-start" + this.rootDir,
|
||||
"file-base-routing-end" + this.rootDir
|
||||
)
|
||||
?.duration?.toFixed(2)
|
||||
performance && performance.mark("file-base-routing-end" + this.rootDir)
|
||||
const timeSpent =
|
||||
performance &&
|
||||
performance
|
||||
.measure(
|
||||
"file-base-routing-measure" + this.rootDir,
|
||||
"file-base-routing-start" + this.rootDir,
|
||||
"file-base-routing-end" + this.rootDir
|
||||
)
|
||||
?.duration?.toFixed(2)
|
||||
|
||||
log({
|
||||
activityId: this.activityId,
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
MedusaApp,
|
||||
moduleLoader,
|
||||
ModulesDefinition,
|
||||
moduleLoader,
|
||||
registerModules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { Express, NextFunction, Request, Response } from "express"
|
||||
|
||||
import databaseLoader, { dataSource } from "./database"
|
||||
import pluginsLoader, { registerPluginModels } from "./plugins"
|
||||
|
||||
import { Connection } from "typeorm"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import { Express, NextFunction, Request, Response } from "express"
|
||||
import { createMedusaContainer } from "medusa-core-utils"
|
||||
import { track } from "medusa-telemetry"
|
||||
import { EOL } from "os"
|
||||
import "reflect-metadata"
|
||||
import requestIp from "request-ip"
|
||||
import { Connection } from "typeorm"
|
||||
import { joinerConfig } from "../joiner-config"
|
||||
import modulesConfig from "../modules-config"
|
||||
import { MedusaContainer } from "../types/global"
|
||||
import { isObject, remoteQueryFetchData } from "../utils"
|
||||
import apiLoader from "./api"
|
||||
import loadConfig from "./config"
|
||||
import databaseLoader, { dataSource } from "./database"
|
||||
import defaultsLoader from "./defaults"
|
||||
import expressLoader from "./express"
|
||||
import featureFlagsLoader from "./feature-flags"
|
||||
import IsolatePricingDomainFeatureFlag from "./feature-flags/isolate-pricing-domain"
|
||||
import IsolateProductDomainFeatureFlag from "./feature-flags/isolate-product-domain"
|
||||
import Logger from "./logger"
|
||||
import { joinerConfig } from "../joiner-config"
|
||||
import loadConfig from "./config"
|
||||
import modelsLoader from "./models"
|
||||
import passportLoader from "./passport"
|
||||
import pgConnectionLoader from "./pg-connection"
|
||||
import pluginsLoader, { registerPluginModels } from "./plugins"
|
||||
import redisLoader from "./redis"
|
||||
import repositoriesLoader from "./repositories"
|
||||
import searchIndexLoader from "./search-index"
|
||||
import servicesLoader from "./services"
|
||||
import strategiesLoader from "./strategies"
|
||||
import subscribersLoader from "./subscribers"
|
||||
import { ConfigModule } from "@medusajs/types"
|
||||
import loadMedusaApp from "./medusa-app"
|
||||
|
||||
type Options = {
|
||||
directory: string
|
||||
@@ -45,34 +44,6 @@ type Options = {
|
||||
isTest: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the modules config from the medusa-config file with the modules config from medusa package
|
||||
* @param modules
|
||||
* @param medusaInternalModulesConfig
|
||||
*/
|
||||
function mergeModulesConfig(
|
||||
modules: ConfigModule["modules"],
|
||||
medusaInternalModulesConfig
|
||||
) {
|
||||
for (const [moduleName, moduleConfig] of Object.entries(modules as any)) {
|
||||
const moduleDefinition = ModulesDefinition[moduleName]
|
||||
|
||||
if (moduleDefinition?.isLegacy) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isModuleEnabled = moduleConfig === true || isObject(moduleConfig)
|
||||
|
||||
if (!isModuleEnabled) {
|
||||
delete medusaInternalModulesConfig[moduleName]
|
||||
} else {
|
||||
medusaInternalModulesConfig[moduleName] = moduleConfig as Partial<
|
||||
InternalModuleDeclaration | ExternalModuleDeclaration
|
||||
>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async ({
|
||||
directory: rootDirectory,
|
||||
expressApp,
|
||||
@@ -138,11 +109,7 @@ export default async ({
|
||||
track("MODULES_INIT_STARTED")
|
||||
await moduleLoader({
|
||||
container,
|
||||
moduleResolutions: registerModules(configModule?.modules, {
|
||||
loadLegacyOnly: featureFlagRouter.isFeatureEnabled(
|
||||
IsolateProductDomainFeatureFlag.key
|
||||
),
|
||||
}),
|
||||
moduleResolutions: registerModules(configModule?.modules),
|
||||
logger: Logger,
|
||||
})
|
||||
const modAct = Logger.success(modulesActivity, "Modules initialized") || {}
|
||||
@@ -167,6 +134,15 @@ export default async ({
|
||||
[ContainerRegistrationKeys.MANAGER]: asValue(dataSource.manager),
|
||||
})
|
||||
|
||||
container.register("remoteQuery", asValue(null)) // ensure remoteQuery is always registered
|
||||
// Only load non legacy modules, the legacy modules (non migrated yet) are retrieved by the registerModule above
|
||||
if (
|
||||
featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) ||
|
||||
featureFlagRouter.isFeatureEnabled(IsolatePricingDomainFeatureFlag.key)
|
||||
) {
|
||||
await loadMedusaApp({ configModule, container })
|
||||
}
|
||||
|
||||
const servicesActivity = Logger.activity(`Initializing services${EOL}`)
|
||||
track("SERVICES_INIT_STARTED")
|
||||
servicesLoader({ container, configModule, isTest })
|
||||
@@ -226,32 +202,5 @@ export default async ({
|
||||
Logger.success(searchActivity, "Indexing event emitted") || {}
|
||||
track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration })
|
||||
|
||||
// Only load non legacy modules, the legacy modules (non migrated yet) are retrieved by the registerModule above
|
||||
if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) {
|
||||
mergeModulesConfig(configModule.modules ?? {}, modulesConfig)
|
||||
|
||||
const { query, modules } = await MedusaApp({
|
||||
modulesConfig,
|
||||
servicesConfig: joinerConfig,
|
||||
remoteFetchData: remoteQueryFetchData(container),
|
||||
sharedContainer: container,
|
||||
injectedDependencies: {
|
||||
[ContainerRegistrationKeys.PG_CONNECTION]: container.resolve(
|
||||
ContainerRegistrationKeys.PG_CONNECTION
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
// Medusa app load all non legacy modules, so we need to register them in the container since they are into their own container
|
||||
// We might decide to do it elsewhere but for now I think it is fine
|
||||
for (const [serviceKey, moduleService] of Object.entries(modules)) {
|
||||
container.register(
|
||||
ModulesDefinition[serviceKey].registrationName,
|
||||
asValue(moduleService)
|
||||
)
|
||||
}
|
||||
container.register("remoteQuery", asValue(query))
|
||||
}
|
||||
|
||||
return { container, dbConnection, app: expressApp }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { CommonTypes, MedusaContainer } from "@medusajs/types"
|
||||
import {
|
||||
MedusaApp,
|
||||
MedusaAppOutput,
|
||||
ModulesDefinition,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import { joinerConfig } from "../joiner-config"
|
||||
import { mergeModulesConfig } from "../utils/merge-modules-config"
|
||||
import modulesConfig from "../modules-config"
|
||||
import { remoteQueryFetchData } from ".."
|
||||
|
||||
export const loadMedusaApp = async (
|
||||
{
|
||||
configModule,
|
||||
container,
|
||||
}: { configModule: CommonTypes.ConfigModule; container: MedusaContainer },
|
||||
config = { registerInContainer: true }
|
||||
): Promise<MedusaAppOutput> => {
|
||||
mergeModulesConfig(configModule.modules ?? {}, modulesConfig)
|
||||
|
||||
const injectedDependencies = {
|
||||
[ContainerRegistrationKeys.PG_CONNECTION]: container.resolve(
|
||||
ContainerRegistrationKeys.PG_CONNECTION
|
||||
),
|
||||
}
|
||||
|
||||
const sharedResourcesConfig = {
|
||||
database: {
|
||||
clientUrl: configModule.projectConfig.database_url,
|
||||
driverOptions: configModule.projectConfig.database_extra,
|
||||
},
|
||||
}
|
||||
|
||||
const medusaApp = await MedusaApp({
|
||||
modulesConfig,
|
||||
servicesConfig: joinerConfig,
|
||||
remoteFetchData: remoteQueryFetchData(container),
|
||||
sharedContainer: container,
|
||||
sharedResourcesConfig,
|
||||
injectedDependencies,
|
||||
})
|
||||
|
||||
if (!config.registerInContainer) {
|
||||
return medusaApp
|
||||
}
|
||||
|
||||
container.register("remoteLink", asValue(medusaApp.link))
|
||||
|
||||
const { query, modules } = medusaApp
|
||||
|
||||
// Medusa app load all non legacy modules, so we need to register them in the container since they are into their own container
|
||||
// We might decide to do it elsewhere but for now I think it is fine
|
||||
for (const [serviceKey, moduleService] of Object.entries(modules)) {
|
||||
container.register(
|
||||
ModulesDefinition[serviceKey].registrationName,
|
||||
asValue(moduleService)
|
||||
)
|
||||
}
|
||||
container.register("remoteQuery", asValue(query))
|
||||
|
||||
return medusaApp
|
||||
}
|
||||
|
||||
export default loadMedusaApp
|
||||
@@ -1,13 +1,13 @@
|
||||
import { asValue, AwilixContainer } from "awilix"
|
||||
import { ConfigModule } from "../types/global"
|
||||
import { ContainerRegistrationKeys, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { AwilixContainer, asValue } from "awilix"
|
||||
import { ConfigModule } from "../types/global"
|
||||
|
||||
type Options = {
|
||||
configModule: ConfigModule
|
||||
container: AwilixContainer
|
||||
}
|
||||
|
||||
export default async ({ container, configModule }: Options): Promise<void> => {
|
||||
export default async ({ container, configModule }: Options): Promise<any> => {
|
||||
if (container.hasRegistration(ContainerRegistrationKeys.PG_CONNECTION)) {
|
||||
return
|
||||
}
|
||||
@@ -36,4 +36,6 @@ export default async ({ container, configModule }: Options): Promise<void> => {
|
||||
ContainerRegistrationKeys.PG_CONNECTION,
|
||||
asValue(pgConnection)
|
||||
)
|
||||
|
||||
return pgConnection
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AwilixContainer } from "awilix"
|
||||
import { IPricingModuleService } from "@medusajs/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"] },
|
||||
{ take: 1 }
|
||||
)
|
||||
|
||||
if (existing.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await pricingModuleService.createRuleTypes([
|
||||
{ name: "region_id", rule_attribute: "region_id" },
|
||||
])
|
||||
}
|
||||
|
||||
const migrate = async function ({ directory }) {
|
||||
const app = express()
|
||||
const { container } = await loaders({
|
||||
directory,
|
||||
expressApp: app,
|
||||
isTest: false,
|
||||
})
|
||||
|
||||
return await createDefaultRuleTypes(container)
|
||||
}
|
||||
|
||||
migrate({ directory: process.cwd() })
|
||||
.then(() => {
|
||||
console.log("Created default rule types")
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("Failed to create rule types")
|
||||
})
|
||||
@@ -0,0 +1,142 @@
|
||||
import { FlagRouter, MedusaError } from "@medusajs/utils"
|
||||
import { IPricingModuleService, MedusaContainer } from "@medusajs/types"
|
||||
|
||||
import { AwilixContainer } from "awilix"
|
||||
import { EntityManager } from "typeorm"
|
||||
import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ProductVariant } from "../models"
|
||||
import { ProductVariantService } from "../services"
|
||||
import { createDefaultRuleTypes } from "./create-default-rule-types"
|
||||
import dotenv from "dotenv"
|
||||
import express from "express"
|
||||
import loadMedusaApp from "../loaders/medusa-app"
|
||||
import loaders from "../loaders"
|
||||
|
||||
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: {
|
||||
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 Promise.all(
|
||||
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(IsolatePricingDomainFeatureFlag.key) &&
|
||||
!featureFlagRouter.isFeatureEnabled(Modules.PRICING)
|
||||
) {
|
||||
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() })
|
||||
@@ -5,9 +5,15 @@ export const PricingServiceMock = {
|
||||
setProductPrices: jest.fn().mockImplementation((prod) => {
|
||||
return Promise.resolve(prod)
|
||||
}),
|
||||
setAdminProductPricing: jest.fn().mockImplementation((prod) => {
|
||||
return Promise.resolve(prod)
|
||||
}),
|
||||
setVariantPrices: jest.fn().mockImplementation(([variant]) => {
|
||||
return Promise.resolve([variant])
|
||||
}),
|
||||
setAdminVariantPricing: jest.fn().mockImplementation(([variant]) => {
|
||||
return Promise.resolve([variant])
|
||||
}),
|
||||
setShippingOptionPrices: jest.fn().mockImplementation((opts) => {
|
||||
return Promise.resolve(opts)
|
||||
}),
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
import _ from "lodash"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { IsNull, Not } from "typeorm"
|
||||
import { PaymentSessionStatus } from "../../models"
|
||||
import TaxCalculationStrategy from "../../strategies/tax-calculation"
|
||||
import { cacheServiceMock } from "../__mocks__/cache"
|
||||
import { NewTotalsService, PricingService, TaxProviderService } from "../index"
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
|
||||
import CartService from "../cart"
|
||||
import { CustomerServiceMock } from "../__mocks__/customer"
|
||||
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||
import { LineItemServiceMock } from "../__mocks__/line-item"
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { LineItemServiceMock } from "../__mocks__/line-item"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { PaymentProviderServiceMock } from "../__mocks__/payment-provider"
|
||||
import { PaymentSessionStatus } from "../../models"
|
||||
import { ProductServiceMock } from "../__mocks__/product"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import { RegionServiceMock } from "../__mocks__/region"
|
||||
import { ShippingOptionServiceMock } from "../__mocks__/shipping-option"
|
||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
import CartService from "../cart"
|
||||
import { NewTotalsService, PricingService, TaxProviderService } from "../index"
|
||||
import SystemTaxService from "../system-tax"
|
||||
import TaxCalculationStrategy from "../../strategies/tax-calculation"
|
||||
import _ from "lodash"
|
||||
import { cacheServiceMock } from "../__mocks__/cache"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
@@ -2657,6 +2658,8 @@ describe("CartService", () => {
|
||||
.register("taxProviderService", asClass(TaxProviderService))
|
||||
.register("newTotalsService", asClass(NewTotalsService))
|
||||
.register("cartService", asClass(CartService))
|
||||
.register("remoteQuery", asValue(null))
|
||||
.register("pricingModuleService", asValue(undefined))
|
||||
.register("pricingService", asClass(PricingService))
|
||||
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ProductVariantService, RegionService, TaxProviderService } from "."
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import {
|
||||
CalculatedPriceSetDTO,
|
||||
IPricingModuleService,
|
||||
PriceSetMoneyAmountDTO,
|
||||
RemoteJoinerQuery,
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/types"
|
||||
import { FlagRouter, removeNullish } from "@medusajs/utils"
|
||||
import {
|
||||
IPriceSelectionStrategy,
|
||||
PriceSelectionContext,
|
||||
} from "../interfaces/price-selection-strategy"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { Product, ProductVariant, Region, ShippingOption } from "../models"
|
||||
import {
|
||||
MoneyAmount,
|
||||
Product,
|
||||
ProductVariant,
|
||||
Region,
|
||||
ShippingOption,
|
||||
} from "../models"
|
||||
import {
|
||||
PricedProduct,
|
||||
PricedShippingOption,
|
||||
@@ -17,7 +25,15 @@ import {
|
||||
ProductVariantPricing,
|
||||
TaxedPricing,
|
||||
} from "../types/pricing"
|
||||
import { ProductVariantService, RegionService, TaxProviderService } from "."
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import IsolatePricingDomainFeatureFlag from "../loaders/feature-flags/isolate-pricing-domain"
|
||||
import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { calculatePriceTaxAmount } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -27,6 +43,8 @@ type InjectedDependencies = {
|
||||
regionService: RegionService
|
||||
priceSelectionStrategy: IPriceSelectionStrategy
|
||||
featureFlagRouter: FlagRouter
|
||||
remoteQuery: RemoteQueryFunction
|
||||
pricingModuleService: IPricingModuleService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,6 +56,8 @@ class PricingService extends TransactionBaseService {
|
||||
protected readonly priceSelectionStrategy: IPriceSelectionStrategy
|
||||
protected readonly productVariantService: ProductVariantService
|
||||
protected readonly featureFlagRouter: FlagRouter
|
||||
protected readonly pricingModuleService: IPricingModuleService
|
||||
protected readonly remoteQuery: RemoteQueryFunction
|
||||
|
||||
constructor({
|
||||
productVariantService,
|
||||
@@ -45,6 +65,8 @@ class PricingService extends TransactionBaseService {
|
||||
regionService,
|
||||
priceSelectionStrategy,
|
||||
featureFlagRouter,
|
||||
remoteQuery,
|
||||
pricingModuleService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -54,6 +76,8 @@ class PricingService extends TransactionBaseService {
|
||||
this.priceSelectionStrategy = priceSelectionStrategy
|
||||
this.productVariantService = productVariantService
|
||||
this.featureFlagRouter = featureFlagRouter
|
||||
this.pricingModuleService = pricingModuleService
|
||||
this.remoteQuery = remoteQuery
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +184,90 @@ class PricingService extends TransactionBaseService {
|
||||
return taxedPricing
|
||||
}
|
||||
|
||||
private async getProductVariantPricingModulePricing_(
|
||||
variantPriceData: {
|
||||
variantId: string
|
||||
quantity?: number
|
||||
}[],
|
||||
context: PricingContext
|
||||
) {
|
||||
const variables = {
|
||||
variant_id: variantPriceData.map((pricedata) => pricedata.variantId),
|
||||
}
|
||||
|
||||
const query = {
|
||||
product_variant_price_set: {
|
||||
__args: variables,
|
||||
fields: ["variant_id", "price_set_id"],
|
||||
},
|
||||
}
|
||||
|
||||
const variantPriceSets = await this.remoteQuery(query)
|
||||
|
||||
const variantIdToPriceSetIdMap: Map<string, string> = new Map(
|
||||
variantPriceSets.map((variantPriceSet) => [
|
||||
variantPriceSet.variant_id,
|
||||
variantPriceSet.price_set_id,
|
||||
])
|
||||
)
|
||||
|
||||
const priceSetIds: string[] = variantPriceSets.map(
|
||||
(variantPriceSet) => variantPriceSet.price_set_id
|
||||
)
|
||||
|
||||
const queryContext: PriceSelectionContext = removeNullish(
|
||||
context.price_selection
|
||||
)
|
||||
|
||||
let priceSets: CalculatedPriceSetDTO[] = []
|
||||
|
||||
if (queryContext.currency_code) {
|
||||
priceSets = (await this.pricingModuleService.calculatePrices(
|
||||
{ id: priceSetIds },
|
||||
{
|
||||
context: queryContext as any,
|
||||
}
|
||||
)) as unknown as CalculatedPriceSetDTO[]
|
||||
}
|
||||
|
||||
const priceSetMap = new Map<string, CalculatedPriceSetDTO>(
|
||||
priceSets.map((priceSet) => [priceSet.id, priceSet])
|
||||
)
|
||||
|
||||
const pricingResultMap = new Map()
|
||||
|
||||
variantPriceData.forEach(({ variantId }) => {
|
||||
const priceSetId = variantIdToPriceSetIdMap.get(variantId)
|
||||
|
||||
const pricingResult: ProductVariantPricing = {
|
||||
prices: [] as MoneyAmount[],
|
||||
original_price: null,
|
||||
calculated_price: null,
|
||||
calculated_price_type: null,
|
||||
original_price_includes_tax: null,
|
||||
calculated_price_includes_tax: null,
|
||||
original_price_incl_tax: null,
|
||||
calculated_price_incl_tax: null,
|
||||
original_tax: null,
|
||||
calculated_tax: null,
|
||||
tax_rates: null,
|
||||
}
|
||||
|
||||
if (priceSetId) {
|
||||
const prices = priceSetMap.get(priceSetId)
|
||||
|
||||
if (prices) {
|
||||
pricingResult.prices = [prices] as MoneyAmount[]
|
||||
pricingResult.original_price = prices.amount
|
||||
pricingResult.calculated_price = prices.amount
|
||||
}
|
||||
}
|
||||
pricingResultMap.set(variantId, pricingResult)
|
||||
})
|
||||
|
||||
return pricingResultMap
|
||||
}
|
||||
|
||||
private async getProductVariantPricing_(
|
||||
data: {
|
||||
variantId: string
|
||||
@@ -167,6 +275,17 @@ class PricingService extends TransactionBaseService {
|
||||
}[],
|
||||
context: PricingContext
|
||||
): Promise<Map<string, ProductVariantPricing>> {
|
||||
if (
|
||||
this.featureFlagRouter.isFeatureEnabled(
|
||||
IsolateProductDomainFeatureFlag.key
|
||||
) &&
|
||||
this.featureFlagRouter.isFeatureEnabled(
|
||||
IsolatePricingDomainFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
return await this.getProductVariantPricingModulePricing_(data, context)
|
||||
}
|
||||
|
||||
const variantsPricing = await this.priceSelectionStrategy
|
||||
.withTransaction(this.activeManager_)
|
||||
.calculateVariantPrice(data, context.price_selection)
|
||||
@@ -509,6 +628,7 @@ class PricingService extends TransactionBaseService {
|
||||
product.variants.map((productVariant): PricedVariant => {
|
||||
const variantPricing = productsVariantsPricingMap.get(product.id)!
|
||||
const pricing = variantPricing[productVariant.id]
|
||||
|
||||
Object.assign(productVariant, pricing)
|
||||
return productVariant as unknown as PricedVariant
|
||||
})
|
||||
@@ -517,6 +637,167 @@ class PricingService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
|
||||
private async getPricingModuleVariantMoneyAmounts(
|
||||
variantIds: string[]
|
||||
): Promise<Map<string, MoneyAmount[]>> {
|
||||
const variables = {
|
||||
variant_id: variantIds,
|
||||
}
|
||||
|
||||
const query = {
|
||||
product_variant_price_set: {
|
||||
__args: variables,
|
||||
fields: ["variant_id", "price_set_id"],
|
||||
},
|
||||
}
|
||||
|
||||
const variantPriceSets = await this.remoteQuery(query)
|
||||
|
||||
const priceSetIdToVariantIdMap: Map<string, string> = new Map(
|
||||
variantPriceSets.map((variantPriceSet) => [
|
||||
variantPriceSet.price_set_id,
|
||||
variantPriceSet.variant_id,
|
||||
])
|
||||
)
|
||||
|
||||
const priceSetIds: string[] = variantPriceSets.map(
|
||||
(variantPriceSet) => variantPriceSet.price_set_id
|
||||
)
|
||||
|
||||
const priceSetMoneyAmounts: PriceSetMoneyAmountDTO[] =
|
||||
await this.pricingModuleService.listPriceSetMoneyAmounts(
|
||||
{
|
||||
price_set_id: priceSetIds,
|
||||
},
|
||||
{
|
||||
relations: [
|
||||
"money_amount",
|
||||
"price_set",
|
||||
"price_rules",
|
||||
"price_rules.rule_type",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const variantIdMoneyAmountMap = priceSetMoneyAmounts.reduce(
|
||||
(map, priceSetMoneyAmount) => {
|
||||
const variantId = priceSetIdToVariantIdMap.get(
|
||||
priceSetMoneyAmount.price_set!.id
|
||||
)
|
||||
if (!variantId) {
|
||||
return map
|
||||
}
|
||||
|
||||
const regionId = priceSetMoneyAmount.price_rules!.find(
|
||||
(pr) => pr.rule_type.rule_attribute === "region_id"
|
||||
)?.value
|
||||
|
||||
const moneyAmount = {
|
||||
...priceSetMoneyAmount.money_amount,
|
||||
region_id: null as null | string,
|
||||
}
|
||||
|
||||
if (regionId) {
|
||||
moneyAmount.region_id = regionId
|
||||
}
|
||||
|
||||
if (map.has(variantId)) {
|
||||
map.get(variantId).push(moneyAmount)
|
||||
} else {
|
||||
map.set(variantId, [moneyAmount])
|
||||
}
|
||||
return map
|
||||
},
|
||||
new Map()
|
||||
)
|
||||
|
||||
return variantIdMoneyAmountMap
|
||||
}
|
||||
|
||||
async setAdminVariantPricing(
|
||||
variants: ProductVariant[],
|
||||
context: PriceSelectionContext = {}
|
||||
): Promise<PricedVariant[]> {
|
||||
if (
|
||||
!this.featureFlagRouter.isFeatureEnabled(
|
||||
IsolatePricingDomainFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
return await this.setVariantPrices(variants, context)
|
||||
}
|
||||
|
||||
const variantIds = variants.map((variant) => variant.id)
|
||||
|
||||
const variantIdMoneyAmountMap =
|
||||
await this.getPricingModuleVariantMoneyAmounts(variantIds)
|
||||
|
||||
return variants.map((variant) => {
|
||||
const pricing: ProductVariantPricing = {
|
||||
prices: variantIdMoneyAmountMap.get(variant.id) ?? [],
|
||||
original_price: null,
|
||||
calculated_price: null,
|
||||
calculated_price_type: null,
|
||||
original_price_includes_tax: null,
|
||||
calculated_price_includes_tax: null,
|
||||
original_price_incl_tax: null,
|
||||
calculated_price_incl_tax: null,
|
||||
original_tax: null,
|
||||
calculated_tax: null,
|
||||
tax_rates: null,
|
||||
}
|
||||
|
||||
Object.assign(variant, pricing)
|
||||
return variant as unknown as PricedVariant
|
||||
})
|
||||
}
|
||||
|
||||
async setAdminProductPricing(
|
||||
products: Product[]
|
||||
): Promise<(Product | PricedProduct)[]> {
|
||||
if (
|
||||
!this.featureFlagRouter.isFeatureEnabled(
|
||||
IsolatePricingDomainFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
return await this.setProductPrices(products)
|
||||
}
|
||||
|
||||
const variantIds = products
|
||||
.map((product) => product.variants.map((variant) => variant.id).flat())
|
||||
.flat()
|
||||
|
||||
const variantIdMoneyAmountMap =
|
||||
await this.getPricingModuleVariantMoneyAmounts(variantIds)
|
||||
|
||||
return products.map((product) => {
|
||||
if (!product?.variants?.length) {
|
||||
return product
|
||||
}
|
||||
|
||||
product.variants.map((productVariant): PricedVariant => {
|
||||
const pricing: ProductVariantPricing = {
|
||||
prices: variantIdMoneyAmountMap.get(productVariant.id) ?? [],
|
||||
original_price: null,
|
||||
calculated_price: null,
|
||||
calculated_price_type: null,
|
||||
original_price_includes_tax: null,
|
||||
calculated_price_includes_tax: null,
|
||||
original_price_incl_tax: null,
|
||||
calculated_price_incl_tax: null,
|
||||
original_tax: null,
|
||||
calculated_tax: null,
|
||||
tax_rates: null,
|
||||
}
|
||||
|
||||
Object.assign(productVariant, pricing)
|
||||
|
||||
return productVariant as unknown as PricedVariant
|
||||
})
|
||||
|
||||
return product
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the prices for a shipping option.
|
||||
* @param shippingOption - the shipping option to get prices for
|
||||
|
||||
@@ -124,3 +124,14 @@ export type PricedVariant = Partial<ProductVariant> & ProductVariantPricing
|
||||
export type PricedProduct = Omit<Partial<Product>, "variants"> & {
|
||||
variants: PricedVariant[]
|
||||
}
|
||||
|
||||
export type VariantPriceSetRes = {
|
||||
id: string
|
||||
title: string
|
||||
price: PriceModulePrice | PriceModulePrice[]
|
||||
}
|
||||
|
||||
type PriceModulePrice = {
|
||||
variant_id: string
|
||||
price_set_id: string
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
ConfigModule,
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { ModulesDefinition } from "@medusajs/modules-sdk"
|
||||
import { isObject } from "./is-object"
|
||||
|
||||
/**
|
||||
* Merge the modules config from the medusa-config file with the modules config from medusa package
|
||||
* @param modules
|
||||
* @param medusaInternalModulesConfig
|
||||
*/
|
||||
export function mergeModulesConfig(
|
||||
modules: ConfigModule["modules"],
|
||||
medusaInternalModulesConfig
|
||||
) {
|
||||
for (const [moduleName, moduleConfig] of Object.entries(modules as any)) {
|
||||
const moduleDefinition = ModulesDefinition[moduleName]
|
||||
|
||||
if (moduleDefinition?.isLegacy) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isModuleEnabled = moduleConfig === true || isObject(moduleConfig)
|
||||
|
||||
if (!isModuleEnabled) {
|
||||
delete medusaInternalModulesConfig[moduleName]
|
||||
} else {
|
||||
medusaInternalModulesConfig[moduleName] = moduleConfig as Partial<
|
||||
InternalModuleDeclaration | ExternalModuleDeclaration
|
||||
>
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user