diff --git a/.changeset/tame-pillows-heal.md b/.changeset/tame-pillows-heal.md new file mode 100644 index 0000000000..bb6b6dd354 --- /dev/null +++ b/.changeset/tame-pillows-heal.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): invalidate price selection caching within update request diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 11b975b3fd..e20f0f23e5 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -44,7 +44,7 @@ describe("/admin/products", () => { dbConnection = await initDb({ cwd }) medusaProcess = await setupServer({ cwd, - env: { MEDUSA_FF_PRODUCT_CATEGORIES: true } + env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }, }) }) @@ -1784,7 +1784,9 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - const variant = response.data.product.variants.find(v => v.id === variantId) + const variant = response.data.product.variants.find( + (v) => v.id === variantId + ) expect(variant.prices.length).toEqual(1) expect(variant.prices).toEqual( expect.arrayContaining([ diff --git a/integration-tests/api/medusa-config.js b/integration-tests/api/medusa-config.js index ee3dc47c9c..ef65d04db4 100644 --- a/integration-tests/api/medusa-config.js +++ b/integration-tests/api/medusa-config.js @@ -4,6 +4,7 @@ const DB_PASSWORD = process.env.DB_PASSWORD const DB_NAME = process.env.DB_TEMP_NAME const redisUrl = process.env.REDIS_URL || "redis://localhost:6379" +const cacheTTL = process.env.CACHE_TTL || 15 module.exports = { plugins: [], @@ -17,11 +18,7 @@ module.exports = { modules: { cacheService: { resolve: "@medusajs/cache-inmemory", - // don't set cache since this is shared between tests - // and since we have "test-product" / "test-variant" as ids - // in a bunch of tests, this could cause that incorrect data is returned - // (e.g. price selection caches calculations under `ps:${variantId}`) - options: { ttl: 0 }, + options: { ttl: cacheTTL }, }, }, } diff --git a/packages/medusa/src/api/routes/admin/variants/list-variants.ts b/packages/medusa/src/api/routes/admin/variants/list-variants.ts index 019eaf9133..f4e323925b 100644 --- a/packages/medusa/src/api/routes/admin/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/admin/variants/list-variants.ts @@ -149,6 +149,7 @@ export default async (req, res) => { currency_code: currencyCode, customer_id: req.validatedQuery.customer_id, include_discount_prices: true, + ignore_cache: true, }) res.json({ diff --git a/packages/medusa/src/interfaces/price-selection-strategy.ts b/packages/medusa/src/interfaces/price-selection-strategy.ts index deec8b350b..02dacfedeb 100644 --- a/packages/medusa/src/interfaces/price-selection-strategy.ts +++ b/packages/medusa/src/interfaces/price-selection-strategy.ts @@ -7,7 +7,7 @@ export interface IPriceSelectionStrategy { /** * Instantiate a new price selection strategy with the active transaction in * order to ensure reads are accurate. - * @param manager EntityManager with the queryrunner of the active transaction + * @param manager EntityManager with the query runner of the active transaction * @returns a new price selection strategy */ withTransaction(manager: EntityManager): IPriceSelectionStrategy @@ -15,15 +15,21 @@ export interface IPriceSelectionStrategy { /** * Calculate the original and discount price for a given variant in a set of * circumstances described in the context. - * @param variant The variant id of the variant for which to retrieve prices + * @param variantId The variant id of the variant for which to retrieve prices * @param context Details relevant to determine the correct pricing of the variant * @return pricing details in an object containing the calculated lowest price, * the default price an all valid prices for the given variant */ calculateVariantPrice( - variant_id: string, + variantId: string, context: PriceSelectionContext ): Promise + + /** + * Notify price selection strategy that variants prices have been updated. + * @param variantIds The ids of the updated variants + */ + onVariantsPricesUpdate(variantIds: string[]): Promise } export abstract class AbstractPriceSelectionStrategy @@ -34,9 +40,13 @@ export abstract class AbstractPriceSelectionStrategy ): IPriceSelectionStrategy public abstract calculateVariantPrice( - variant_id: string, + variantId: string, context: PriceSelectionContext ): Promise + + public async onVariantsPricesUpdate(variantIds: string[]): Promise { + return void 0 + } } export function isPriceSelectionStrategy( @@ -57,6 +67,7 @@ export type PriceSelectionContext = { currency_code?: string include_discount_prices?: boolean tax_rates?: TaxServiceRate[] + ignore_cache?: boolean } enum DefaultPriceType { diff --git a/packages/medusa/src/services/__tests__/product-variant.js b/packages/medusa/src/services/__tests__/product-variant.js index d853d3e86e..e9afaf8416 100644 --- a/packages/medusa/src/services/__tests__/product-variant.js +++ b/packages/medusa/src/services/__tests__/product-variant.js @@ -649,10 +649,14 @@ describe("ProductVariantService", () => { .fn() .mockImplementation(() => Promise.resolve()) - const regionService = { + const priceSelectionStrategy = { withTransaction: function () { return this }, + onVariantsPricesUpdate: (variantIds) => Promise.resolve(), + } + + const regionService = { list: jest.fn().mockImplementation((config) => { const idOrIds = config.id @@ -679,6 +683,7 @@ describe("ProductVariantService", () => { manager: MockManager, eventBusService, regionService, + priceSelectionStrategy, moneyAmountRepository, }) diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index d52169d334..3a84abaf66 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -426,6 +426,7 @@ class PricingService extends TransactionBaseService { context: PriceSelectionContext = {} ): Promise { const pricingContext = await this.collectPricingContext(context) + return await Promise.all( variants.map(async (variant) => { const variantPricing = await this.getProductVariantPricing( diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index cf7b97537f..63ca5f69d3 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -367,6 +367,10 @@ class ProductVariantService extends TransactionBaseService { await this.setCurrencyPrice(variantId, price) } } + + await this.priceSelectionStrategy_ + .withTransaction(manager) + .onVariantsPricesUpdate([variantId]) }) } diff --git a/packages/medusa/src/strategies/price-selection.ts b/packages/medusa/src/strategies/price-selection.ts index 26d01deedc..c815e36713 100644 --- a/packages/medusa/src/strategies/price-selection.ts +++ b/packages/medusa/src/strategies/price-selection.ts @@ -51,11 +51,13 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { context: PriceSelectionContext ): Promise { const cacheKey = this.getCacheKey(variant_id, context) - const cached = await this.cacheService_ - .get(cacheKey) - .catch(() => void 0) - if (cached) { - return cached + if (!context.ignore_cache) { + const cached = await this.cacheService_ + .get(cacheKey) + .catch(() => void 0) + if (cached) { + return cached + } } let result @@ -235,6 +237,14 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { return result } + public async onVariantsPricesUpdate(variantIds: string[]): Promise { + await Promise.all( + variantIds.map( + async (id: string) => await this.cacheService_.invalidate(`ps:${id}:*`) + ) + ) + } + private getCacheKey( variantId: string, context: PriceSelectionContext diff --git a/packages/medusa/src/subscribers/pricing.ts b/packages/medusa/src/subscribers/pricing.ts deleted file mode 100644 index 27daa4f5f1..0000000000 --- a/packages/medusa/src/subscribers/pricing.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ICacheService } from "@medusajs/types" -import { EventBusService, ProductVariantService } from "../services" - -type ProductVariantUpdatedEventData = { - id: string - product_id: string - fields: string[] -} - -class PricingSubscriber { - protected readonly eventBus_: EventBusService - protected readonly cacheService_: ICacheService - - constructor({ eventBusService, cacheService }) { - this.eventBus_ = eventBusService - this.cacheService_ = cacheService - - this.eventBus_.subscribe( - ProductVariantService.Events.UPDATED, - async (data) => { - const { id, fields } = data as ProductVariantUpdatedEventData - if (fields.includes("prices")) { - await this.cacheService_.invalidate(`ps:${id}:*`) - } - } - ) - } -} - -export default PricingSubscriber