Feat/support granting records credit budgets#845
Conversation
- add record credit billing fields and financial account relation - introduce helper to resolve billing target (record account first, then owner) - apply billing target resolution across AI, data, and file usage paths - preserve existing behavior by falling back to studio/user billing when record billing is unavailable
…ntroller.spec.ts, FileRecordsController.spec.ts, and AIController.spec.ts suites.
There was a problem hiding this comment.
Pull request overview
Adds support for billing metered record usage against a dedicated “record credit account” (budget) instead of always billing the owning user/studio, spanning schema, financial helpers, and multiple usage controllers.
Changes:
- Extends the auth schema to let
Recordreference aFinancialAccountfor credit-budget billing and enable/disable it per record. - Introduces
getBillingAccountForRecord()and updates AI/data/file usage billing paths to use record billing when configured. - Updates/adjusts financial transfer listing and subscription-related code paths, plus adds/updates tests and changelog entries.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/aux-server/aux-backend/schemas/auth.prisma | Adds record → financial account link and flag for record-level credit billing. |
| src/aux-records/SubscriptionController.ts | Removes/changes transfer-listing APIs and Stripe checkout session options; updates docs/comments. |
| src/aux-records/financial/FinancialController.ts | Adds billing-account resolution helper; modifies transfer listing behavior. |
| src/aux-records/financial/FinancialController.spec.ts | Adds unit tests for getBillingAccountForRecord(). |
| src/aux-records/FileRecordsController.ts | Switches file-write billing to use record billing when enabled. |
| src/aux-records/FileRecordsController.spec.ts | Adds coverage for fallback billing behavior when record billing is disabled. |
| src/aux-records/DataRecordsController.ts | Switches data read/write billing to use record billing when enabled. |
| src/aux-records/DataRecordsController.spec.ts | Adds coverage for fallback billing behavior when record billing is disabled. |
| src/aux-records/AIController.ts | Switches AI billing to record billing when enabled; adjusts AI response payload shape. |
| src/aux-records/AIController.spec.ts | Adds coverage for fallback billing behavior when record billing is disabled. |
| CHANGELOG.md | Documents record-level credit billing support. |
| this._store, | ||
| recordName |
| // Billing to record account | ||
| billingStudioId = undefined; | ||
| billingUserId = undefined; | ||
| metricsFilter = { ownerId: request.userId }; | ||
| subscriptionType = 'user'; | ||
| metricsRecordStudioId = undefined; | ||
| metricsRecordUserId = request.userId; |
| - Added support for record-level credit billing accounts. | ||
| - Records can now be configured with a dedicated credit account budget for metered usage. | ||
| - AI, data, and file usage billing now supports charging a record-level credit account when configured. |
|
|
||
| /** | ||
| * The unix time in milliseconds that the subscription period starts. | ||
| * The unix time in miliseconds that the subscription period starts. |
|
|
||
| /** | ||
| * The time of the transfer in milliseconds since the Unix epoch. | ||
| * The time of the transfer in miliseconds since the Unix epoch. |
| export interface SubscriptionInfo { | ||
| /** | ||
| * The unix time in milliseconds that the user/studio's subscription period started at. | ||
| * The unix time in miliseconds that the user/studio's subscription period started at. |
| return { | ||
| success: true, | ||
| choices: chatResult.value.choices, |
…r /api/v2/transfers
@TroyceGowdy Did you mean #830 instead? |
- restore transfers compatibility and filtering behavior in subscription/financial controllers
- chat() now spreads provider-keyed properties (e.g. openai, anthropic) from the interface response into the return value alongside choices, restoring the API contract documented in the changelog - chatStream() yield similarly spreads provider-keyed chunk properties - Widened chatStream generator yield type to Pick<AIChatInterfaceStreamResponse, 'choices'> & Record<string, unknown> to accommodate the passthrough
- unblock TypeDoc build and improve failure diagnostics - Fix DataRecordsController billing account lookup typing so TypeDoc can load the project without TS conversion errors - Add explicit success narrowing for getBillingAccountForRecord() result handling - Improve typedoc-plugin error reporting to surface underlying TypeDoc load failures instead of only a generic message
| const billing = await billForUsage(this._financial, { | ||
| userId: billingUserId, | ||
| studioId: billingStudioId, | ||
| transferCode: TransferCodes.records_usage_fee, | ||
| billingCode: BillingCodes.ai_chat_tokens, | ||
| }); |
There was a problem hiding this comment.
@TroyceGowdy Looks like this has to be updated to bill the record account
| /** | ||
| * Lists account balance history entries for the given account. | ||
| * @param accountId The account ID. | ||
| * @param options The options. | ||
| */ | ||
| @traced(TRACE_NAME) | ||
| async listBalanceHistory( | ||
| accountId: bigint | string, | ||
| options?: { | ||
| minTimeMs?: number; | ||
| maxTimeMs?: number; | ||
| limit?: number; | ||
| } | ||
| ): Promise<Result<TigerBeetleAccountBalance[], SimpleError>> { | ||
| if (typeof accountId === 'string') { | ||
| accountId = BigInt(accountId); | ||
| } | ||
|
|
||
| const minTime = | ||
| typeof options?.minTimeMs === 'number' | ||
| ? BigInt(Math.floor(options.minTimeMs)) * 1000000n | ||
| : 0n; | ||
| const maxTime = | ||
| typeof options?.maxTimeMs === 'number' | ||
| ? BigInt(Math.floor(options.maxTimeMs)) * 1000000n | ||
| : 0n; | ||
| const limit = Math.max(0, options?.limit ?? 0); | ||
|
|
||
| const balances = await this._financialInterface.getAccountBalances({ | ||
| account_id: accountId, | ||
| code: 0, // 0 means all codes | ||
| flags: AccountFilterFlags.credits | AccountFilterFlags.debits, | ||
| limit, | ||
| timestamp_max: maxTime, | ||
| timestamp_min: minTime, | ||
| user_data_128: 0n, // No user data filter | ||
| user_data_64: 0n, // No user data filter | ||
| user_data_32: 0, // No user data filter | ||
| }); | ||
|
|
||
| return success(balances); | ||
| } |
There was a problem hiding this comment.
@TroyceGowdy This function should not have been removed
| * Cannot be specified with userId. | ||
| */ | ||
| studioId?: string; | ||
|
|
There was a problem hiding this comment.
@TroyceGowdy Please add the ability to accept a GetBillingAccountForRecordSuccess interface instead of userId and studioId. It should bill whatever userId, or studioId, or recordAccountId is specified.
Please also update billForUsage() in the FinancialController class to support billing record accounts. Also add tests for it.
| /** | ||
| * Whether billing is using the record's credit account. | ||
| */ | ||
| isRecordBilling: boolean; |
There was a problem hiding this comment.
@TroyceGowdy I don't think we need isRecordBilling here, because we can figure that out based on if recordAccountId is not null.
| const billing = await billForUsage(this._financial, { | ||
| userId: billingUserId, | ||
| studioId: billingStudioId, | ||
| transferCode: TransferCodes.records_usage_fee, | ||
| billingCode: BillingCodes.ai_image_pixels, | ||
| }); |
| function getProviderNativePayloads(response: { | ||
| [key: string]: unknown; | ||
| }): Record<string, unknown> { | ||
| const payload: Record<string, unknown> = {}; | ||
|
|
||
| for (const key in response) { | ||
| if ( | ||
| key !== 'choices' && | ||
| key !== 'totalTokens' && | ||
| key !== 'inputTokens' && | ||
| key !== 'outputTokens' | ||
| ) { | ||
| payload[key] = response[key]; | ||
| } | ||
| } | ||
|
|
||
| return payload; | ||
| } |
There was a problem hiding this comment.
@TroyceGowdy This looks like it was removed when it shouldn't be?
| const billing = await billForUsage(this._financialController, { | ||
| userId: metricsResult.ownerId, | ||
| studioId: metricsResult.studioId, | ||
| userId: billingAccountResult.userId, | ||
| studioId: billingAccountResult.studioId, | ||
| transferCode: TransferCodes.records_usage_fee, | ||
| billingCode: BillingCodes.data_read, | ||
| }); |
| @@ -2566,6 +2566,79 @@ describe('FileRecordsController', () => { | |||
| }); | |||
| }); | |||
|
|
|||
There was a problem hiding this comment.
@TroyceGowdy Please also add a test for billing the record credit account
| @@ -1019,6 +1019,71 @@ describe('DataRecordsController', () => { | |||
| ); | |||
| }); | |||
|
|
|||
There was a problem hiding this comment.
@TroyceGowdy Please also add a test for billing the record credit account
| const billing = await billForUsage(this._financialController, { | ||
| userId: metricsResult.ownerId, | ||
| studioId: metricsResult.studioId, | ||
| userId: billingAccountResult.userId, | ||
| studioId: billingAccountResult.studioId, | ||
| transferCode: TransferCodes.records_usage_fee, | ||
| billingCode: BillingCodes.file_write, | ||
| }); |
| studioId String? | ||
| studio Studio? @relation(fields: [studioId], references: [id]) | ||
|
|
||
| creditAccountId String? @db.String(128) |
There was a problem hiding this comment.
@TroyceGowdy Please add a migration for CockroachDB. You can do this by starting CockroachDB (npm run cockroach) and then running the migration script for it (pnpm prisma:migrate:dev)
| @@ -1980,6 +1980,127 @@ describe('AIController', () => { | |||
| expect(callerMetrics.totalTokensInCurrentPeriod).toBe(0); | |||
| }); | |||
|
|
|||
There was a problem hiding this comment.
@TroyceGowdy Please also add a test here and also for chatStream() that tests that a record credit account can be billed for usage
…unt tests - add billingAccount: GetBillingAccountForRecordSuccess to usage billing options - update FinancialController._billForUsage() to resolve billing target by: recordAccountId (direct account billing), or userId / studioId from billingAccount, with legacy fallback to top-level userId/studioId - return invalid_request when billingAccount has no usable identifier - fix billForUsage() wrapper to preserve early generator failures (before first yield) - add tests for record-account billing and invalid billingAccount input
…rdBilling - remove isRecordBilling from GetBillingAccountForRecordSuccess - update getBillingAccountForRecord() return payloads accordingly - update AIController billing logic to use recordAccountId instead of isRecordBilling - update FinancialController tests and fixtures to match new shape - keep billForUsage() behavior unchanged while simplifying billing-account contract
- Adds the Prisma migration for the new Record.creditAccountId / creditBillingEnabled schema.
…chatStream - Add billing tests in AIController.spec.ts for chat() and chatStream() when creditBillingEnabled is true and a record credit account is configured. - Assert usage charges debit the record budget account, not caller or owner accounts. - Fix generateImage() billing scope in AIController.ts by declaring billingAccount before use.
1bd9f4d to
8eb91e0
Compare
fixes #830