Billing - fixes (#19867)
- Uniformize credit formating : In UI, 1$=1credit. In BE 1 UI credit = 1_000_000 BE "crédits" - Add crédit rollover information + Link to documentation + Documentation update <img width="291" height="317" alt="Screenshot 2026-04-17 at 18 22 59" src="https://github.com/user-attachments/assets/2519fb9f-159d-4c85-95f4-a6e005a8a1a3" /> <img width="848" height="763" alt="Screenshot 2026-04-17 at 14 12 20" src="https://github.com/user-attachments/assets/a3cc0874-f275-49ea-819f-305ec314bdfe" /> <img width="797" height="757" alt="Screenshot 2026-04-17 at 14 12 13" src="https://github.com/user-attachments/assets/9048409b-d5a2-435a-b735-70370705e668" /> - Enable direct top-up (or subscription if in trial) from AI chat <img width="333" height="215" alt="Screenshot 2026-04-17 at 22 52 00" src="https://github.com/user-attachments/assets/7a20c627-2806-4bcf-a037-b45752232be9" /> <img width="457" height="769" alt="Screenshot 2026-04-17 at 22 51 41" src="https://github.com/user-attachments/assets/d2a90c1b-271f-4fe9-8891-baeb2fabb86d" /> - Inform users if credit limit is reached - Banner <img width="1130" height="127" alt="Screenshot 2026-04-17 at 19 15 11" src="https://github.com/user-attachments/assets/30723e5e-c07e-462f-8eb8-e08f52bbab1c" />
This commit is contained in:
@@ -6,4 +6,5 @@ export enum UsageResourceType {
|
||||
APP = 'APP',
|
||||
STORAGE = 'STORAGE',
|
||||
API = 'API',
|
||||
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
|
||||
}
|
||||
|
||||
+2
-9
@@ -33,7 +33,6 @@ type PeriodParams = {
|
||||
periodStart: Date;
|
||||
periodEnd: Date;
|
||||
operationTypes?: string[];
|
||||
useDollarMode?: boolean;
|
||||
};
|
||||
|
||||
const ALLOWED_GROUP_BY_FIELDS = [
|
||||
@@ -137,7 +136,6 @@ export class UsageAnalyticsService {
|
||||
periodEnd,
|
||||
groupByField,
|
||||
operationTypes,
|
||||
useDollarMode = false,
|
||||
extraWhere = '',
|
||||
extraParams,
|
||||
}: PeriodParams & {
|
||||
@@ -173,8 +171,6 @@ export class UsageAnalyticsService {
|
||||
LIMIT ${BREAKDOWN_QUERY_LIMIT}
|
||||
`;
|
||||
|
||||
const convert = useDollarMode ? toDollars : toDisplayCredits;
|
||||
|
||||
const rows = await this.clickHouseService.select<BreakdownRowMicro>(query, {
|
||||
workspaceId,
|
||||
periodStart: formatDateForClickHouse(periodStart),
|
||||
@@ -187,7 +183,7 @@ export class UsageAnalyticsService {
|
||||
|
||||
return rows.map((row) => ({
|
||||
key: row.key,
|
||||
creditsUsed: convert(row.creditsUsedMicro),
|
||||
creditsUsed: row.creditsUsedMicro,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -196,7 +192,6 @@ export class UsageAnalyticsService {
|
||||
periodStart,
|
||||
periodEnd,
|
||||
operationTypes,
|
||||
useDollarMode = false,
|
||||
extraWhere = '',
|
||||
extraParams,
|
||||
}: PeriodParams & {
|
||||
@@ -222,8 +217,6 @@ export class UsageAnalyticsService {
|
||||
ORDER BY date ASC
|
||||
`;
|
||||
|
||||
const convert = useDollarMode ? toDollars : toDisplayCredits;
|
||||
|
||||
const rows = await this.clickHouseService.select<TimeSeriesRowMicro>(
|
||||
query,
|
||||
{
|
||||
@@ -239,7 +232,7 @@ export class UsageAnalyticsService {
|
||||
|
||||
return rows.map((row) => ({
|
||||
date: row.date,
|
||||
creditsUsed: convert(row.creditsUsedMicro),
|
||||
creditsUsed: row.creditsUsedMicro,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,21 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import { UsageAnalyticsDTO } from 'src/engine/core-modules/usage/dtos/usage-analytics.dto';
|
||||
import { UsageAnalyticsInput } from 'src/engine/core-modules/usage/dtos/inputs/usage-analytics.input';
|
||||
import {
|
||||
type UsageBreakdownItem,
|
||||
UsageAnalyticsService,
|
||||
} from 'src/engine/core-modules/usage/services/usage-analytics.service';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UsageAnalyticsInput } from 'src/engine/core-modules/usage/dtos/inputs/usage-analytics.input';
|
||||
import { UsageAnalyticsDTO } from 'src/engine/core-modules/usage/dtos/usage-analytics.dto';
|
||||
import {
|
||||
UsageAnalyticsService,
|
||||
type UsageBreakdownItem,
|
||||
} from 'src/engine/core-modules/usage/services/usage-analytics.service';
|
||||
import { toDisplayCredits } from 'src/engine/core-modules/usage/utils/to-display-credits.util';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
|
||||
@MetadataResolver()
|
||||
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
|
||||
@@ -30,7 +30,6 @@ import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorato
|
||||
export class UsageResolver {
|
||||
constructor(
|
||||
private readonly usageAnalyticsService: UsageAnalyticsService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
) {}
|
||||
@@ -51,14 +50,12 @@ export class UsageResolver {
|
||||
|
||||
const periodStart = input?.periodStart ?? defaultPeriodStart;
|
||||
const periodEnd = input?.periodEnd ?? defaultPeriodEnd;
|
||||
const useDollarMode = !this.twentyConfigService.get('IS_BILLING_ENABLED');
|
||||
|
||||
const periodParams = {
|
||||
workspaceId: workspace.id,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
operationTypes: input?.operationTypes ?? undefined,
|
||||
useDollarMode,
|
||||
};
|
||||
|
||||
const [usageByUser, usageByOperationType, usageByModel, timeSeries] =
|
||||
@@ -78,10 +75,22 @@ export class UsageResolver {
|
||||
);
|
||||
|
||||
const result: UsageAnalyticsDTO = {
|
||||
usageByUser: resolvedUsageByUser,
|
||||
usageByOperationType,
|
||||
usageByModel,
|
||||
timeSeries,
|
||||
usageByUser: resolvedUsageByUser.map((item) => ({
|
||||
...item,
|
||||
creditsUsed: toDisplayCredits(item.creditsUsed),
|
||||
})),
|
||||
usageByOperationType: usageByOperationType.map((item) => ({
|
||||
...item,
|
||||
creditsUsed: toDisplayCredits(item.creditsUsed),
|
||||
})),
|
||||
usageByModel: usageByModel.map((item) => ({
|
||||
...item,
|
||||
creditsUsed: toDisplayCredits(item.creditsUsed),
|
||||
})),
|
||||
timeSeries: timeSeries.map((point) => ({
|
||||
...point,
|
||||
creditsUsed: toDisplayCredits(point.creditsUsed),
|
||||
})),
|
||||
periodStart,
|
||||
periodEnd,
|
||||
};
|
||||
@@ -101,7 +110,10 @@ export class UsageResolver {
|
||||
|
||||
result.userDailyUsage = {
|
||||
userWorkspaceId: input.userWorkspaceId,
|
||||
dailyUsage,
|
||||
dailyUsage: dailyUsage.map((point) => ({
|
||||
...point,
|
||||
creditsUsed: toDisplayCredits(point.creditsUsed),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -1,9 +1,6 @@
|
||||
// Internal credits use micro-precision: $1 = 1,000,000 internal credits
|
||||
// Display credits are 1000x coarser: $1 = 1,000 display credits
|
||||
// This mirrors the "micro" pattern in payment systems (e.g. microdollars → dollars)
|
||||
export const INTERNAL_CREDITS_PER_DISPLAY_CREDIT = 1000;
|
||||
export const INTERNAL_CREDITS_PER_DISPLAY_CREDIT = 1_000_000;
|
||||
|
||||
// Converts internal (high-precision) credits to user-facing display credits.
|
||||
// Rounds to 1 decimal place for clean display (e.g. 7500 → 7.5).
|
||||
export const toDisplayCredits = (internalCredits: number): number =>
|
||||
Math.round((internalCredits / INTERNAL_CREDITS_PER_DISPLAY_CREDIT) * 10) / 10;
|
||||
internalCredits / INTERNAL_CREDITS_PER_DISPLAY_CREDIT;
|
||||
|
||||
Reference in New Issue
Block a user