Create billing customer at signup so onboarding rewards are credited (#22633)

## Problem

Onboarding credit rewards (install apps, import contacts, invite team)
were silently dropped. They credit the workspace balance via
`billingCustomer.increment(...)`, but no `billingCustomer` row exists
until the plan step (it's created lazily when the first subscription is
set up, which is after those steps). So the increment affected 0 rows
and the credit was lost. A user installing 3 apps saw only the trial
grant, not the expected +1.5 credits.

## Fix

Create the Stripe customer + `billingCustomer` row eagerly at signup via
a new `BillingCreditService.ensureBillingCustomer`, called from
`signUpOnNewWorkspace` after the workspace transaction commits. It is
idempotent, guarded by `IS_BILLING_ENABLED`, and non-blocking (failures
are logged, not thrown). The later subscription flow reuses this
customer (no duplicate Stripe customer), and trial eligibility is
unchanged since the customer has no subscriptions yet.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22633?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Raphaël Bosi
2026-07-07 16:51:53 +02:00
committed by GitHub
parent 35d3f9b89d
commit 2c0e0b2eac
4 changed files with 135 additions and 0 deletions
@@ -108,6 +108,9 @@ const createSignInUpServiceForTests = () => {
{
creditWorkspaceBalance: jest.fn(),
} as any,
{
isBillingEnabled: jest.fn(),
} as any,
{
createQueryRunner: jest.fn(() => queryRunnerMock),
} as any,
@@ -25,6 +25,7 @@ import {
} from 'src/engine/core-modules/app-token/app-token.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { BillingCreditService } from 'src/engine/core-modules/billing/services/billing-credit.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import {
AuthException,
AuthExceptionCode,
@@ -93,6 +94,7 @@ export class SignInUpService {
private readonly enterprisePlanService: EnterprisePlanService,
private readonly eventLogEmitterService: EventLogEmitterService,
private readonly billingCreditService: BillingCreditService,
private readonly billingService: BillingService,
@InjectDataSource()
private readonly dataSource: DataSource,
) {}
@@ -708,6 +710,14 @@ export class SignInUpService {
.createContext({ workspaceId })
.insertWorkspaceEvent(WORKSPACE_CREATED_EVENT, {});
if (this.billingService.isBillingEnabled()) {
await this.billingService.ensureBillingCustomer({
userEmail: email,
workspaceId: workspace.id,
workspaceDisplayName: workspace.displayName,
});
}
return { user, workspace };
} catch (error) {
const isSubdomainConflict =