Skip to content

Conversation

AMoreaux
Copy link
Contributor

@AMoreaux AMoreaux commented Sep 2, 2025

… prices for metered billing

@AMoreaux AMoreaux self-assigned this Sep 2, 2025
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Summary

This PR introduces comprehensive automation for Stripe price management in Twenty's metered billing system, along with significant UI improvements for the billing interface. The core addition is a Node.js script (update-stripe-prices.js) that automates the creation and archival of Stripe prices for metered billing based on workflow node runs. The script creates tiered pricing structures with 12 different credit tiers (ranging from 5,000 to 7.5M credits) for both monthly and yearly subscriptions, implementing proper idempotency using SHA-256 hashes to prevent duplicate price creation.

The frontend changes focus on improving the billing page organization and user experience. Components are being restructured with internal implementation details moved to a dedicated internal/ directory, following common patterns for separating public APIs from private components. The MeteredPriceSelector component receives a major UX overhaul, transforming from a simple dropdown to a safer two-step confirmation process where users must first select a plan, then explicitly click an Upgrade/Downgrade button, and finally confirm their choice in a modal.

The billing system is also being simplified by removing support for daily and weekly subscription intervals, keeping only monthly and yearly periods which align with the actual supported intervals in the codebase. The SettingsBillingCreditsSection component is updated to enable previously commented-out metered billing functionality, including the ability to dynamically select different credit plans with proper error handling and confirmation flows.

These changes work together to create a more robust and user-friendly metered billing system, with automated Stripe price management on the backend and an improved confirmation-based user interface on the frontend.

Confidence score: 3/5

  • This PR requires careful review due to potential breaking changes and complex billing logic that could affect revenue
  • Score reflects concerns about top-level await usage, enum value removal without migration checks, and removal of internationalization support
  • Pay close attention to the Stripe script, enum changes, and MeteredPriceSelector component modifications

8 files reviewed, 3 comments

Edit Code Review Bot Settings | Greptile

@@ -0,0 +1,175 @@
// Usage: STRIPE_API_KEY=sk_live_xxx PRODUCT_ID=prod_xxx node create-and-archive-prices.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Comment mentions wrong filename 'create-and-archive-prices.js' but actual file is 'update-stripe-prices.js'

Suggested change
// Usage: STRIPE_API_KEY=sk_live_xxx PRODUCT_ID=prod_xxx node create-and-archive-prices.js
// Usage: STRIPE_API_KEY=sk_live_xxx PRODUCT_ID=prod_xxx node update-stripe-prices.js

throw new Error('Meter not found');
}

const formatCredits = (credits) => credits.toLocaleString('de-DE'); // exemple: 7.500.000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Typo in comment: 'exemple' should be 'example'

Suggested change
const formatCredits = (credits) => credits.toLocaleString('de-DE'); // exemple: 7.500.000
const formatCredits = (credits) => credits.toLocaleString('de-DE'); // example: 7.500.000


const makeNickname = (r) => `${formatCredits(r.credits)} Credits`;

const makeIdemKey = (r) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Function name 'makeIdemKey' uses abbreviation. Consider 'makeIdempotencyKey' for clarity

Context Used: Context - Avoid using abbreviations in the codebase; prefer full descriptive names, e.g., use 'record' instead of 'r'. (link)

Copy link
Contributor

github-actions bot commented Sep 2, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:56852

This environment will automatically shut down when the PR is closed or after 5 hours.

@AMoreaux AMoreaux changed the title feat(billing): add script to automate creation and archival of Stripe… feat(billing): refacto billing Sep 5, 2025
…page

# Conflicts:
#	packages/twenty-front/src/generated-metadata/graphql.ts
#	packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx
#	packages/twenty-front/src/modules/billing/components/internal/MeteredPriceSelector.tsx
…line pricing structures, and enhance subscription schedule handling
Copy link
Contributor

github-actions bot commented Sep 5, 2025

📊 API Changes Report

GraphQL Schema Changes

GraphQL Schema Changes

[log]
Detected the following changes (28) between schemas:

[log] ✖ Field baseProduct was removed from object type BillingPlanOutput
[log] ✖ Field BillingPlanOutput.meteredProducts changed type from [BillingProduct!]! to [BillingMeteredProduct!]!
[log] ✖ Field otherLicensedProducts was removed from object type BillingPlanOutput
[log] ✖ Field tiersMode was removed from object type BillingPriceMeteredDTO
[log] ✖ Type BillingPriceOutput was removed
[log] ✖ Type BillingPriceTiersMode was removed
[log] ✖ Type BillingPriceUnionDTO was removed
[log] ✖ Field prices was removed from object type BillingProduct
[log] ✖ Field BillingSubscription.billingSubscriptionItems changed type from [BillingSubscriptionItem!] to [BillingSubscriptionItemDTO!]
[log] ✖ Type BillingSubscriptionItem was removed
[log] ✖ Field switchToYearlyInterval was removed from object type Mutation
[log] ✖ Field updateSubscriptionItemPrice was removed from object type Mutation
[log] ✖ Field listAvailableMeteredBillingPrices was removed from object type Query
[log] ✖ Field plans was removed from object type Query
[log] ✖ Enum value Day was removed from enum SubscriptionInterval
[log] ✖ Enum value Week was removed from enum SubscriptionInterval
[log] ✔ Type BillingLicensedProduct was added
[log] ✔ Type BillingMeteredProduct was added
[log] ✔ Field licensedProducts was added to object type BillingPlanOutput
[log] ✔ Field BillingPriceMeteredDTO.tiers changed type from [BillingPriceTierDTO!] to [BillingPriceTierDTO!]!
[log] ✔ Type BillingProductDTO was added
[log] ✔ Field phases was added to object type BillingSubscription
[log] ✔ Type BillingSubscriptionItemDTO was added
[log] ✔ Type BillingSubscriptionSchedulePhase was added
[log] ✔ Type BillingSubscriptionSchedulePhaseItem was added
[log] ✔ Field setMeteredSubscriptionPrice was added to object type Mutation
[log] ✔ Field toggleSubscriptionInterval was added to object type Mutation
[log] ✔ Field listPlans was added to object type Query
[error] Detected 16 breaking changes
⚠️ Breaking changes or errors detected in GraphQL schema

[log] 
Detected the following changes (28) between schemas:

[log] ✖  Field baseProduct was removed from object type BillingPlanOutput
[log] ✖  Field BillingPlanOutput.meteredProducts changed type from [BillingProduct!]! to [BillingMeteredProduct!]!
[log] ✖  Field otherLicensedProducts was removed from object type BillingPlanOutput
[log] ✖  Field tiersMode was removed from object type BillingPriceMeteredDTO
[log] ✖  Type BillingPriceOutput was removed
[log] ✖  Type BillingPriceTiersMode was removed
[log] ✖  Type BillingPriceUnionDTO was removed
[log] ✖  Field prices was removed from object type BillingProduct
[log] ✖  Field BillingSubscription.billingSubscriptionItems changed type from [BillingSubscriptionItem!] to [BillingSubscriptionItemDTO!]
[log] ✖  Type BillingSubscriptionItem was removed
[log] ✖  Field switchToYearlyInterval was removed from object type Mutation
[log] ✖  Field updateSubscriptionItemPrice was removed from object type Mutation
[log] ✖  Field listAvailableMeteredBillingPrices was removed from object type Query
[log] ✖  Field plans was removed from object type Query
[log] ✖  Enum value Day was removed from enum SubscriptionInterval
[log] ✖  Enum value Week was removed from enum SubscriptionInterval
[log] ✔  Type BillingLicensedProduct was added
[log] ✔  Type BillingMeteredProduct was added
[log] ✔  Field licensedProducts was added to object type BillingPlanOutput
[log] ✔  Field BillingPriceMeteredDTO.tiers changed type from [BillingPriceTierDTO!] to [BillingPriceTierDTO!]!
[log] ✔  Type BillingProductDTO was added
[log] ✔  Field phases was added to object type BillingSubscription
[log] ✔  Type BillingSubscriptionItemDTO was added
[log] ✔  Type BillingSubscriptionSchedulePhase was added
[log] ✔  Type BillingSubscriptionSchedulePhaseItem was added
[log] ✔  Field setMeteredSubscriptionPrice was added to object type Mutation
[log] ✔  Field toggleSubscriptionInterval was added to object type Mutation
[log] ✔  Field listPlans was added to object type Query
[error] Detected 16 breaking changes
Error generating diff

GraphQL Metadata Schema Changes

GraphQL Metadata Schema Changes

[log]
Detected the following changes (28) between schemas:

[log] ✖ Field baseProduct was removed from object type BillingPlanOutput
[log] ✖ Field BillingPlanOutput.meteredProducts changed type from [BillingProduct!]! to [BillingMeteredProduct!]!
[log] ✖ Field otherLicensedProducts was removed from object type BillingPlanOutput
[log] ✖ Field tiersMode was removed from object type BillingPriceMeteredDTO
[log] ✖ Type BillingPriceOutput was removed
[log] ✖ Type BillingPriceTiersMode was removed
[log] ✖ Type BillingPriceUnionDTO was removed
[log] ✖ Field prices was removed from object type BillingProduct
[log] ✖ Field BillingSubscription.billingSubscriptionItems changed type from [BillingSubscriptionItem!] to [BillingSubscriptionItemDTO!]
[log] ✖ Type BillingSubscriptionItem was removed
[log] ✖ Field switchToYearlyInterval was removed from object type Mutation
[log] ✖ Field updateSubscriptionItemPrice was removed from object type Mutation
[log] ✖ Field listAvailableMeteredBillingPrices was removed from object type Query
[log] ✖ Field plans was removed from object type Query
[log] ✖ Enum value Day was removed from enum SubscriptionInterval
[log] ✖ Enum value Week was removed from enum SubscriptionInterval
[log] ✔ Type BillingLicensedProduct was added
[log] ✔ Type BillingMeteredProduct was added
[log] ✔ Field licensedProducts was added to object type BillingPlanOutput
[log] ✔ Field BillingPriceMeteredDTO.tiers changed type from [BillingPriceTierDTO!] to [BillingPriceTierDTO!]!
[log] ✔ Type BillingProductDTO was added
[log] ✔ Field phases was added to object type BillingSubscription
[log] ✔ Type BillingSubscriptionItemDTO was added
[log] ✔ Type BillingSubscriptionSchedulePhase was added
[log] ✔ Type BillingSubscriptionSchedulePhaseItem was added
[log] ✔ Field setMeteredSubscriptionPrice was added to object type Mutation
[log] ✔ Field toggleSubscriptionInterval was added to object type Mutation
[log] ✔ Field listPlans was added to object type Query
[error] Detected 16 breaking changes
⚠️ Breaking changes or errors detected in GraphQL metadata schema

[log] 
Detected the following changes (28) between schemas:

[log] ✖  Field baseProduct was removed from object type BillingPlanOutput
[log] ✖  Field BillingPlanOutput.meteredProducts changed type from [BillingProduct!]! to [BillingMeteredProduct!]!
[log] ✖  Field otherLicensedProducts was removed from object type BillingPlanOutput
[log] ✖  Field tiersMode was removed from object type BillingPriceMeteredDTO
[log] ✖  Type BillingPriceOutput was removed
[log] ✖  Type BillingPriceTiersMode was removed
[log] ✖  Type BillingPriceUnionDTO was removed
[log] ✖  Field prices was removed from object type BillingProduct
[log] ✖  Field BillingSubscription.billingSubscriptionItems changed type from [BillingSubscriptionItem!] to [BillingSubscriptionItemDTO!]
[log] ✖  Type BillingSubscriptionItem was removed
[log] ✖  Field switchToYearlyInterval was removed from object type Mutation
[log] ✖  Field updateSubscriptionItemPrice was removed from object type Mutation
[log] ✖  Field listAvailableMeteredBillingPrices was removed from object type Query
[log] ✖  Field plans was removed from object type Query
[log] ✖  Enum value Day was removed from enum SubscriptionInterval
[log] ✖  Enum value Week was removed from enum SubscriptionInterval
[log] ✔  Type BillingLicensedProduct was added
[log] ✔  Type BillingMeteredProduct was added
[log] ✔  Field licensedProducts was added to object type BillingPlanOutput
[log] ✔  Field BillingPriceMeteredDTO.tiers changed type from [BillingPriceTierDTO!] to [BillingPriceTierDTO!]!
[log] ✔  Type BillingProductDTO was added
[log] ✔  Field phases was added to object type BillingSubscription
[log] ✔  Type BillingSubscriptionItemDTO was added
[log] ✔  Type BillingSubscriptionSchedulePhase was added
[log] ✔  Type BillingSubscriptionSchedulePhaseItem was added
[log] ✔  Field setMeteredSubscriptionPrice was added to object type Mutation
[log] ✔  Field toggleSubscriptionInterval was added to object type Mutation
[log] ✔  Field listPlans was added to object type Query
[error] Detected 16 breaking changes
Error generating diff

⚠️ Please review these API changes carefully before merging.

⚠️ Breaking Change Protocol

Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.

🔄 Options:

  1. If this IS a breaking change: Add "breaking" to your PR title and add BREAKING CHANGE: to your commit message
  2. If this is NOT a breaking change: The API diff tool may have false positives - please review carefully

For breaking changes, add to commit message:

feat: add new API endpoint

BREAKING CHANGE: removed deprecated field from User schema

@charlesBochet
Copy link
Member

@AMoreaux your tests are red!

[selectedPriceId, meteredBillingPrices],
);

const isChanged = Boolean(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean() not standard

selectedPriceId !== currentMeteredBillingPrice?.stripePriceId,
);

const isUpgrade = useMemo(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's not add useMemos, they are likely not useful

setSelectedPriceId(priceId);
};

const confirmModalId = 'metered-price-change-confirmation-modal';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a constant and should be UPPER_CASE

isTrialPeriod?: boolean;
};

export const PlanTag = ({ plan, isTrialPeriod = false }: PlanTagProps) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component is returning two tags and called PlanTag, this is a bit confusing

`;

const StyledIconLabelContainer = styled.div`
align-items: center;
gap: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
width: 120px;
min-width: 0; /* allow label ellipsis within grid column */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised here, we already have components to handle Ellipsis and we should re-use them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TextWithOverflowingToolTip for instance

import { isDefined } from 'twenty-shared/utils';
import { useRecoilValue } from 'recoil';

export const useBillingPlan = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this hook is problematic to me:

  • it's an umbrella hook doing many things, this means that using it will trigger a re-render nightmare!
  • all these getters are not idiomatic to me: the issue is that these are functions and that we use the function return in react which is new for react engine at each re-renders and therefore can trigger a re-render itself

I would recommend creating hooks (maybe not one for everything but more and to return the things directly
For instance:
usePlans()
useProducts()

useProduct(planKey)
etc...

…ow`, remove redundant utils, and add tests for billing helpers
…ependencies, and update related components
…d `min-width` style in `SubscriptionInfoRowContainer`
…n `useBillingWording`, and clean up unused variables
…ervice` and clean up unused variable in subscription service test
…ry, and add debug logging in billing settings
…dLabel` to use options object for `decimals`
…d refactor `useBillingWording` for improved date handling
FelixMalfait and others added 10 commits September 18, 2025 23:33
Big refacto/cleanup that addresses a performance issue and many things
that were dirty as we added and added layers over months.

It's not perfect yet but I think it goes in the right direction
… detection/ folder

- Moved all detect* functions to utils/detection/ subfolder
- Updated all import paths throughout the codebase
- Fixed TypeScript errors in test files by using proper Jest mock typing
- Updated formatDateISOStringToDateTimeSimplified test to use TimeFormat enum values
- Improved code organization and maintainability
…lean up trial period logic, and update billing price entity relations with nullable modifications
@FelixMalfait FelixMalfait merged commit 43e0cd5 into main Sep 19, 2025
56 of 57 checks passed
@FelixMalfait FelixMalfait deleted the feat/improve-billing-page branch September 19, 2025 09:25
prastoin added a commit that referenced this pull request Sep 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants