From 726b1373d9396b33892c09b13e1bcf68d9f094a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 20 Feb 2026 09:44:11 +0100 Subject: [PATCH] fix: convert metered tier upTo from internal to display credits in listPlans (#18108) ## Summary - The `listPlans` GraphQL query was returning metered tier `upTo` values in internal credit units (1000x the display value), causing "Credits by period" and "Credit Plan" dropdowns to show "50M" instead of "50k" - Applied the existing `INTERNAL_CREDITS_PER_DISPLAY_CREDIT` (1000) divisor in `formatBillingDatabasePriceToMeteredPriceDTO`, matching the conversion already used in `getMeteredProductsUsage` resolver - Updated frontend mock data to reflect the corrected display-unit values ## Test plan - [x] Unit tests pass for `format-database-product-to-graphql-dto.util` - [x] Unit tests pass for `metered-credit.service` and `billing-credit-rollover.service` - [ ] Verify billing page shows correct credit amounts (50k, not 50M) for "Credits by period" and "Credit Plan" Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../src/testing/mock-data/billing-plans.ts | 48 +++++++-------- ...tabase-product-to-graphql-dto.util.spec.ts | 59 ++++++++++++++++++- ...at-database-product-to-graphql-dto.util.ts | 3 +- 3 files changed, 82 insertions(+), 28 deletions(-) diff --git a/packages/twenty-front/src/testing/mock-data/billing-plans.ts b/packages/twenty-front/src/testing/mock-data/billing-plans.ts index 64aa36b688..40c53b251b 100644 --- a/packages/twenty-front/src/testing/mock-data/billing-plans.ts +++ b/packages/twenty-front/src/testing/mock-data/billing-plans.ts @@ -58,7 +58,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 999000, unitAmount: null, - upTo: 7500000000, + upTo: 7500000, }, { __typename: 'BillingPriceTier', @@ -78,7 +78,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 399000, unitAmount: null, - upTo: 2600000000, + upTo: 2600000, }, { __typename: 'BillingPriceTier', @@ -98,7 +98,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 199000, unitAmount: null, - upTo: 1200000000, + upTo: 1200000, }, { __typename: 'BillingPriceTier', @@ -118,7 +118,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 99000, unitAmount: null, - upTo: 540000000, + upTo: 540000, }, { __typename: 'BillingPriceTier', @@ -138,7 +138,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 29000, unitAmount: null, - upTo: 130000000, + upTo: 130000, }, { __typename: 'BillingPriceTier', @@ -158,7 +158,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 0, unitAmount: null, - upTo: 50000000, + upTo: 50000, }, { __typename: 'BillingPriceTier', @@ -178,7 +178,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 99900, unitAmount: null, - upTo: 700000000, + upTo: 700000, }, { __typename: 'BillingPriceTier', @@ -198,7 +198,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 39900, unitAmount: null, - upTo: 240000000, + upTo: 240000, }, { __typename: 'BillingPriceTier', @@ -218,7 +218,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 19900, unitAmount: null, - upTo: 110000000, + upTo: 110000, }, { __typename: 'BillingPriceTier', @@ -238,7 +238,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 9900, unitAmount: null, - upTo: 50000000, + upTo: 50000, }, { __typename: 'BillingPriceTier', @@ -258,7 +258,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 2900, unitAmount: null, - upTo: 10000000, + upTo: 10000, }, { __typename: 'BillingPriceTier', @@ -278,7 +278,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 0, unitAmount: null, - upTo: 5000000, + upTo: 5000, }, { __typename: 'BillingPriceTier', @@ -348,7 +348,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 999000, unitAmount: null, - upTo: 7500000000, + upTo: 7500000, }, { __typename: 'BillingPriceTier', @@ -368,7 +368,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 399000, unitAmount: null, - upTo: 2600000000, + upTo: 2600000, }, { __typename: 'BillingPriceTier', @@ -388,7 +388,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 199000, unitAmount: null, - upTo: 1200000000, + upTo: 1200000, }, { __typename: 'BillingPriceTier', @@ -408,7 +408,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 99000, unitAmount: null, - upTo: 540000000, + upTo: 540000, }, { __typename: 'BillingPriceTier', @@ -428,7 +428,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 29000, unitAmount: null, - upTo: 130000000, + upTo: 130000, }, { __typename: 'BillingPriceTier', @@ -448,7 +448,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 0, unitAmount: null, - upTo: 50000000, + upTo: 50000, }, { __typename: 'BillingPriceTier', @@ -468,7 +468,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 99900, unitAmount: null, - upTo: 700000000, + upTo: 700000, }, { __typename: 'BillingPriceTier', @@ -488,7 +488,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 39900, unitAmount: null, - upTo: 240000000, + upTo: 240000, }, { __typename: 'BillingPriceTier', @@ -508,7 +508,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 19900, unitAmount: null, - upTo: 110000000, + upTo: 110000, }, { __typename: 'BillingPriceTier', @@ -528,7 +528,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 9900, unitAmount: null, - upTo: 50000000, + upTo: 50000, }, { __typename: 'BillingPriceTier', @@ -548,7 +548,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 2900, unitAmount: null, - upTo: 10000000, + upTo: 10000, }, { __typename: 'BillingPriceTier', @@ -568,7 +568,7 @@ export const mockBillingPlans = { __typename: 'BillingPriceTier', flatAmount: 0, unitAmount: null, - upTo: 5000000, + upTo: 5000, }, { __typename: 'BillingPriceTier', diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts index 5fbf886245..8a60c22577 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts @@ -31,7 +31,11 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => { { interval: SubscriptionInterval.Month, tiers: [ - { up_to: 10, flat_amount: 500, unit_amount: null }, + { + up_to: 10000000, + flat_amount: 500, + unit_amount: null, + }, { up_to: null, flat_amount: null, unit_amount: 0.001 }, ], stripePriceId: 'price_metered1', @@ -81,7 +85,11 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => { { interval: SubscriptionInterval.Month, tiers: [ - { up_to: 10, flat_amount: 500, unit_amount: null }, + { + up_to: 10000000, + flat_amount: 500, + unit_amount: null, + }, { up_to: null, flat_amount: null, unit_amount: 0.001 }, ], stripePriceId: 'price_metered1', @@ -91,7 +99,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => { prices: [ { tiers: [ - { upTo: 10, flatAmount: 500, unitAmount: null }, + { upTo: 10000, flatAmount: 500, unitAmount: null }, { upTo: null, flatAmount: null, unitAmount: 0.001 }, ], recurringInterval: SubscriptionInterval.Month, @@ -103,4 +111,49 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => { ], }); }); + + it('should convert internal credits to display credits in metered tier upTo', () => { + const mockPlan = { + planKey: BillingPlanKey.PRO, + licensedProducts: [], + meteredProducts: [ + { + id: 'product-2', + name: 'Test Metered Product', + billingPrices: [ + { + interval: SubscriptionInterval.Month, + tiers: [ + { + up_to: 50000000, + flat_amount: 0, + unit_amount: null, + }, + { + up_to: null, + flat_amount: null, + unit_amount: null, + }, + ], + stripePriceId: 'price_metered1', + priceUsageType: BillingUsageType.METERED, + }, + ], + }, + ], + }; + + const result = formatBillingDatabaseProductToGraphqlDTO( + mockPlan as unknown as BillingGetPlanResult, + ); + + const meteredPrices = result.meteredProducts[0].prices; + + expect(meteredPrices![0].tiers[0]).toEqual( + expect.objectContaining({ upTo: 50000 }), + ); + expect(meteredPrices![0].tiers[1]).toEqual( + expect.objectContaining({ upTo: null }), + ); + }); }); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts index ba75b18e0a..cd5c9dcf98 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts @@ -7,6 +7,7 @@ import { type BillingPriceEntity } from 'src/engine/core-modules/billing/entitie import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum'; import { type BillingGetPlanResult } from 'src/engine/core-modules/billing/types/billing-get-plan-result.type'; +import { toDisplayCredits } from 'src/engine/core-modules/billing/utils/to-display-credits.util'; export const formatBillingDatabaseProductToGraphqlDTO = ( plan: BillingGetPlanResult, @@ -42,7 +43,7 @@ const formatBillingDatabasePriceToMeteredPriceDTO = ( return { tiers: billingPrice?.tiers?.map((tier) => ({ - upTo: tier.up_to, + upTo: tier.up_to !== null ? toDisplayCredits(tier.up_to) : tier.up_to, flatAmount: tier.flat_amount, unitAmount: tier.unit_amount, })) ?? [],