Enterprise Post-Paid Billing
Enterprise customers receive unlimited platform access without upfront payment. They are hand-picked, trusted scopes — either an organization (company-scoped) or an individual (personal, companyId=null) — whose subscriptions are managed by a super admin. At the end of each month, an invoice is generated based on actual AI usage (token costs from CV parsing, agent chat, matching, and other operations). The scope has 5 days to pay. If payment is not received, platform access is blocked until the invoice is settled.
Per the standing rule "Plans are user-based: Subscriptions allow companyId=null; never require a company to buy a plan", an enterprise subscription can be created without a company and tied directly to a billing owner (userId). The entire lifecycle — create, approve, block, restore, invoice, usage breakdown — supports both company-scoped and personal subscriptions.
Architecture
The enterprise billing system differs from the standard prepaid model in two fundamental ways:
- No credit balance -- enterprise companies bypass all credit checks. The
BillingGatereturnsbalance=infinityfor POSTPAID subscriptions. - Usage-based invoicing -- AI operations are logged to
AIOperationLogwithout credit deduction. These logs serve as the source of truth for monthly invoice generation.
How It Works
1. Subscription Creation
An enterprise subscription can be created in two ways:
Admin-initiated (immediate activation):
A super admin calls adminCreateEnterpriseSubscription with the plan, billing owner, and optionally a company. When companyId is provided, the subscription is company-scoped; when omitted, it is a personal subscription owned by the billing owner (Subscription.companyId = NULL, Subscription.userId = billingOwnerId). The subscription is immediately set to ACTIVE with isActive: true. Any existing active subscriptions in the same scope (company or personal) are automatically canceled.
User-initiated (requires approval):
A user calls requestEnterpriseSubscription. A company member passes companyId (validated against UserCompanyRole); a user without a company omits it to request a personal subscription. Either path creates a subscription with PENDING_APPROVAL status and isActive: false. The admin is notified and must explicitly approve the request via adminApproveEnterpriseSubscription.
2. Unlimited Access
Once active, the enterprise company has unlimited access to all platform features:
- All 16 plan limits are set to
-1(unlimited). - No
creditsPerMonthallocation is needed (set to0). - The
BillingGate.pre_check()in background-tasks detects thePOSTPAIDbilling mode and returns immediately withbalance=infinity. - No credit balance row is required for the company.
3. Usage Logging
Every billable AI operation (CV extraction, job parsing, meeting insight, text rewrite, agent chat, face extraction) is logged to the AIOperationLog table. For POSTPAID companies, the BillingGate.check_and_consume() method:
- Calculates the credit cost based on token usage and configured rates.
- Writes the
AIOperationLogentry withmetadata.billing_mode = "POSTPAID". - Does not deduct from any credit balance.
- Returns
balance_after = infinity.
This logging happens regardless of the BILLING_ENABLED environment variable. Enterprise usage is always tracked because AIOperationLog is the source of truth for invoice generation.
4. Monthly Invoice Generation
A cron job runs on the 1st of each month at 3:00 AM UTC (0 3 1 * *). It:
- Finds all active POSTPAID subscriptions (both company-scoped and personal — no
companyId IS NOT NULLfilter). - For each scope, queries
AIOperationLogfor the previous calendar month (grouped byoperationType), filtered bycompanyIdfor company-scoped subs or by{companyId IS NULL, userId}for personal subs. - Calculates the total cost from
creditsCostfields on successful operations. - If total usage is zero, skips invoice generation for that scope.
- Creates a Stripe invoice with
collection_method: send_invoiceanddays_until_due: 5. The Stripe customer name is the company name (company-scoped) or the billing owner's full name / email (personal). - Creates a local
Invoicerecord with the billing period, due date,companyId(orNULL), anduserId = billingOwnerId. - Emits the
enterprise.invoice.generatedevent (triggers email and in-app notification to the billing owner).
Stripe sends the billing owner an email with a hosted payment link.
5. Overdue Check and Access Blocking
A second cron job runs daily at 10:00 AM UTC (0 10 * * *). It:
- Finds overdue enterprise invoices — identified by
billingPeriodStart IS NOT NULLand the linkedSubscription.Plan.billingMode = 'POSTPAID'. Includes both company-scoped (companyIdset) and personal (companyId IS NULL) invoices. - Updates the invoice status to
OVERDUE. - Blocks the scope: finds the matching active POSTPAID subscription (by
companyIdor{userId, companyId: null}) and sets its status toUNPAID/isActive: false. - Emits
enterprise.invoice.overdueandenterprise.access.blockedevents withcompanyId: string | null.
When a scope is blocked, the BillingGate.pre_check() in background-tasks detects the UNPAID status and raises a SubscriptionInactiveError. This check is enforced even when BILLING_ENABLED is set to false — blocked enterprise scopes (company or personal) cannot bypass access control.
6. Payment and Access Restoration
Access is restored through one of three paths:
- Stripe webhook — When the billing owner pays the Stripe invoice, the
invoice.paidwebhook fires. The handler detects the enterprise invoice (no Stripe subscription ID, local invoice withbillingPeriodStartset), looks up theUNPAIDPOSTPAID subscription bycompanyIdwhen present or by{userId: localInvoice.userId, companyId: null}for personal subs, restores it toACTIVE, and emitsenterprise.access.restored. - Admin manual override —
adminMarkInvoicePaid(invoiceId)updates the invoice toPAIDand restores the subscription in the matching scope. - Admin direct restore —
adminRestoreEnterpriseAccess(input: { companyId | userId })directly restores access without modifying the invoice.
Plan Configuration
The Enterprise plan is configured with the following properties:
| Field | Value | Description |
|---|---|---|
name | Enterprise | Plan display name |
price | 0 | No recurring charge (usage-based) |
billingMode | POSTPAID | Distinguishes from standard prepaid plans |
creditsPerMonth | 0 | No upfront credit allocation |
| All 16 feature limits | -1 | Unlimited access to all features |
trialDays | 0 | No trial period (admin-managed) |
The billingMode field on the Plan model is what drives all enterprise-specific behavior. Both the backend (subscription management, invoice generation) and background-tasks (billing gate enforcement, usage logging) check this field to determine the billing path.
Subscription Lifecycle
Admin-initiated flow skips PENDING_APPROVAL and goes directly to ACTIVE:
Status Definitions
| Status | isActive | Meaning |
|---|---|---|
PENDING_APPROVAL | false | User requested, awaiting admin approval |
ACTIVE | true | Full platform access, AI operations logged for invoicing |
UNPAID | false | Invoice overdue, all platform features blocked |
CANCELED | false | Rejected request or replaced by a new subscription |
Admin API
All enterprise mutations and queries require JwtAuthGuard + SuperAdminGuard (except requestEnterpriseSubscription, which is available to authenticated users).
Mutations
adminCreateEnterpriseSubscription
Creates an enterprise subscription immediately in ACTIVE status. companyId is optional — when omitted, the subscription is a personal sub tied to the billing owner. Cancels any existing active subscriptions in the same scope.
# Company-scoped
mutation {
adminCreateEnterpriseSubscription(input: {
companyId: "company-uuid"
planId: "enterprise-plan-uuid"
billingOwnerId: "user-uuid"
}) {
id
status
isActive
startDate
Plan { name billingMode }
}
}
# Personal (no company)
mutation {
adminCreateEnterpriseSubscription(input: {
planId: "enterprise-plan-uuid"
billingOwnerId: "user-uuid"
}) { id status isActive }
}
input CreateEnterpriseSubscriptionInput {
companyId: ID # Optional — omit for a personal enterprise subscription
planId: ID!
billingOwnerId: ID!
}
adminApproveEnterpriseSubscription
Approves a pending enterprise request. Requires specifying the billing owner (may differ from the requesting user). Cancels any other active subscriptions for the company.
mutation {
adminApproveEnterpriseSubscription(input: {
subscriptionId: "subscription-uuid"
billingOwnerId: "user-uuid"
}) {
id
status
isActive
}
}
adminRejectEnterpriseSubscription
Rejects a pending enterprise request. Sets status to CANCELED. Emits enterprise.subscription.rejected event with the optional reason.
mutation {
adminRejectEnterpriseSubscription(
subscriptionId: "subscription-uuid"
reason: "Company does not meet enterprise criteria"
) {
id
status
}
}
adminBlockEnterpriseAccess
Manually blocks access for an enterprise scope. Sets the matching subscription to UNPAID with isActive: false. Exactly one of companyId or userId must be provided.
# Block a company-scoped enterprise sub
mutation {
adminBlockEnterpriseAccess(input: { companyId: "company-uuid" }) {
id status isActive
}
}
# Block a personal enterprise sub
mutation {
adminBlockEnterpriseAccess(input: { userId: "user-uuid" }) {
id status isActive
}
}
input EnterpriseAccessScopeInput {
companyId: ID # Provide one
userId: ID # OR the other — exactly one is required
}
adminRestoreEnterpriseAccess
Manually restores access for a blocked enterprise scope. Same EnterpriseAccessScopeInput shape as block.
# Restore a company-scoped sub
mutation {
adminRestoreEnterpriseAccess(input: { companyId: "company-uuid" }) {
id status isActive
}
}
# Restore a personal sub
mutation {
adminRestoreEnterpriseAccess(input: { userId: "user-uuid" }) {
id status isActive
}
}
adminGenerateEnterpriseInvoice
Generates an invoice on demand for a specific billing period. Useful for generating invoices outside the monthly cron cycle or re-generating for a specific date range. Exactly one of companyId or userId must be provided.
# Company-scoped invoice
mutation {
adminGenerateEnterpriseInvoice(input: {
companyId: "company-uuid"
startDate: "2026-01-01T00:00:00Z"
endDate: "2026-01-31T23:59:59Z"
}) {
id amount currency status dueDate
billingPeriodStart billingPeriodEnd stripeInvoiceId
}
}
# Personal (no company) invoice — billed to the user directly
mutation {
adminGenerateEnterpriseInvoice(input: {
userId: "user-uuid"
startDate: "2026-01-01T00:00:00Z"
endDate: "2026-01-31T23:59:59Z"
}) { id amount status dueDate }
}
input GenerateEnterpriseInvoiceInput {
companyId: ID # One of
userId: ID # the two
startDate: DateTime!
endDate: DateTime!
}
adminMarkInvoicePaid
Manual payment override. Marks the invoice as PAID and restores access if the company was blocked.
mutation {
adminMarkInvoicePaid(invoiceId: "invoice-uuid") {
success
message
}
}
requestEnterpriseSubscription
Available to any authenticated user (not admin-only). Creates a PENDING_APPROVAL subscription request.
mutation {
requestEnterpriseSubscription(input: {
companyId: "company-uuid"
planId: "enterprise-plan-uuid"
}) {
id
status # PENDING_APPROVAL
isActive # false
}
}
The requesting user must belong to the target company (validated via UserCompanyRole). The company must not already have an active subscription or a pending enterprise request.
Queries
adminEnterpriseSubscriptions
Lists all enterprise subscriptions (all statuses), with company and subscriber details.
query {
adminEnterpriseSubscriptions {
id
status
isActive
startDate
companyId
Plan { name billingMode }
Company { id companyName billingOwnerId }
SubscribedBy { id email firstName lastName }
}
}
adminEnterpriseUsageBreakdown
Returns a usage breakdown for an enterprise scope over a specific period. This is a preview of what the invoice would contain. Scope is XOR: provide either companyId or userId.
# Company-scoped usage
query {
adminEnterpriseUsageBreakdown(
companyId: "company-uuid"
startDate: "2026-01-01T00:00:00Z"
endDate: "2026-01-31T23:59:59Z"
) {
companyId companyName userId userName
periodStart periodEnd
lineItems { operationType operationCount totalCost totalInputTokens totalOutputTokens }
totalAmount currency
}
}
# Personal usage — companyId/companyName return null; userId/userName are populated
query {
adminEnterpriseUsageBreakdown(
userId: "user-uuid"
startDate: "2026-01-01T00:00:00Z"
endDate: "2026-01-31T23:59:59Z"
) {
userId userName totalAmount currency
lineItems { operationType operationCount totalCost }
}
}
The EnterpriseUsageBreakdown type has nullable scope fields:
type EnterpriseUsageBreakdown {
companyId: String # null for personal
companyName: String # null for personal
userId: String # null for company-scoped
userName: String # null for company-scoped
periodStart: DateTime!
periodEnd: DateTime!
lineItems: [EnterpriseUsageLineItem!]!
totalAmount: Float!
currency: String!
}
Invoice Generation Details
Cron Schedule
| Job | Schedule | Description |
|---|---|---|
| Invoice generation | 0 3 1 * * (1st of month, 3 AM UTC) | Generates invoices for previous month's usage |
| Overdue check | 0 10 * * * (daily, 10 AM UTC) | Blocks companies with past-due invoices |
Invoice Creation Process
- Calculate the previous month's period:
periodStartis the 1st at 00:00 UTC,periodEndis the last day at 23:59:59.999 UTC. - Query
AIOperationLogwithgroupBy(['operationType']), filtering bycompanyId, date range, andstatus: SUCCESS. - Sum
creditsCostacross all operation types for the total invoice amount. - If total is zero (no billable usage), skip invoice generation.
- Look up the billing owner from the subscription (
subscribedByIdoruserId). - Create Stripe invoice line items -- one per operation type, with the format
"CV Extraction -- 42 operations". - Create a Stripe invoice with
collection_method: send_invoiceanddays_until_due: 5. Stripe metadata includestype: enterprise_usageand the company name. - Finalize and send the Stripe invoice (Stripe emails the billing owner with a payment link).
- Create a local
Invoicerecord withPENDINGstatus, linking to the subscription, company, and billing period.
Idempotency
Invoice generation is idempotent at two levels:
- Application level — before creating, the service checks for an existing invoice matching the scope:
{companyId, billingPeriodStart, billingPeriodEnd}for company-scoped, or{companyId: NULL, userId: billingOwnerId, billingPeriodStart, billingPeriodEnd}for personal. - Database level — the unique constraint
@@unique([companyId, billingPeriodStart, billingPeriodEnd])on theInvoicetable catches concurrent generation attempts for company-scoped invoices (Prisma error P2002 → the service re-fetches and returns the existing row).
PostgreSQL treats NULL as distinct in unique constraints, so two concurrent generateMonthlyInvoice calls for the same personal user + period would not collide on the DB unique constraint. The app-level pre-check prevents duplicates for a single cron run, and the monthly cron iterates subscriptions serially — but if personal invoice generation ever becomes parallel across multiple processes, a separate idempotency guard (e.g. an advisory lock keyed on userId + period) will be needed.
Stripe Invoice Resilience
If Stripe invoice creation fails (API error, network issue), the local Invoice record is still created without a stripeInvoiceId. This allows admins to:
- See the invoice in the admin panel.
- Manually generate a Stripe invoice later.
- Use
adminMarkInvoicePaidfor payments received outside Stripe.
Background Tasks Integration
BillingGate Behavior for POSTPAID
The BillingGate class in background-tasks handles POSTPAID companies differently at each step. It uses three internal helper methods for clean code reuse.
pre_check()
# Simplified flow:
# 1. Validate billing context (company_id or user_id must be present)
has_context = await cls._require_billing_context(company_id, user_id, operation_type)
# 2. Resolve billing mode from active subscription.
# Now accepts user_id so personal POSTPAID subs (companyId IS NULL) are
# resolved correctly — previously this path silently fell back to PREPAID.
billing_mode, has_subscription = await cls._resolve_billing_mode(company_id, user_id)
# Returns tuple: ("PREPAID"|"POSTPAID", True|False)
if billing_mode == "POSTPAID":
# Active POSTPAID subscription found (company or personal) — allow immediately.
# No plan limit check, no credit balance check.
return True
# 3. No active subscription? Check for blocked enterprise in either scope.
if not has_subscription:
await cls._check_enterprise_blocked(company_id, user_id)
# Raises SubscriptionInactiveError if UNPAID POSTPAID exists
_resolve_billing_mode(company_id, user_id) resolution order:
- If
company_id→SubscriptionModel.get_active_subscription(company_id). This is now a single CTE that resolves the responsible user viaCOALESCE(Company.billingOwnerId, earliest user with 'Company Admin' role on that company)and then JOINs Subscription/Plan. On a hit, return its billing mode. - If that company-side lookup misses (or no
company_idwas passed), fall through toSubscriptionModel.get_active_user_subscription(user_id)for the caller's personal subscription (Subscription WHERE userId = $1 AND companyId IS NULL AND isActive AND status IN ('ACTIVE','TRIALING')). On a hit, return its billing mode. - Otherwise →
("PREPAID", False).
The Company Admin fallback inside step 1 covers legacy rows where Company.billingOwnerId was never set (companies created before the auto-populate work, seeded fixtures, etc.). Only the Company Admin role qualifies — Recruiter, Internal Sales, and regular members do not. Ties are broken by earliest UserCompanyRole.createdAt. The parallel backend resolver (PlanLimitsService.getCompanyPlanLimits) uses the same COALESCE so both services agree.
The step-2 fall-through covers the enterprise-employee case: a user holds a personal POSTPAID plan (e.g. via adminCreateEnterpriseSubscription with no companyId) but operates inside a company that itself has no subscription and no Company Admin. Without it, the gate would return PREPAID and the operation would fail against an empty company credit balance.
For blocked scopes (subscription status UNPAID), the active-subscription queries return None because they filter by status IN ('ACTIVE', 'TRIALING'). The _check_enterprise_blocked(company_id, user_id) helper then queries either SubscriptionModel.has_unpaid_postpaid_subscription(company_id) or has_unpaid_postpaid_user_subscription(user_id) and raises SubscriptionInactiveError. This check runs even when BILLING_ENABLED is false.
check_and_consume()
# Simplified flow for POSTPAID companies:
if billing_mode == "POSTPAID":
# Log to AIOperationLog -- source of truth for invoicing
await credit_service.record_consumption(
billing_mode="POSTPAID", # Skips credit deduction
...
)
return {"balance_after": infinity}
The record_consumption() method in CreditService:
- Calculates the credit cost from token usage (same formula as PREPAID).
- Writes the
AIOperationLogentry withcredits_costset to the calculated amount. - Adds
billing_mode: "POSTPAID"to the metadata JSON. - Does not touch
CreditBalanceorCreditTransactiontables. - Returns
balance_after = float("inf").
POSTPAID operations are always logged to AIOperationLog regardless of the BILLING_ENABLED flag. This is because the AIOperationLog is the source of truth for monthly invoice generation. Disabling billing should not silently skip enterprise usage tracking.
Agent Tools
The AI agent tools extract_cv and parse_job (app/services/agent/tool_service.py) forward the conversation's user_id alongside company_id when delegating to CVExtractionService / JobParsingService. This is required so that a personal-enterprise user calling these tools has their usage correctly attributed and gated through the personal-POSTPAID path. Tool payload shape:
payload = {
"id": f"agent-cv-{int(time.time())}",
"file_url": file_url,
"companyId": self.company_id,
"userId": self.user_id, # ← required for personal enterprise billing
"sequential": True,
}
Backend Plan-Limit Guard
The backend PlanLimitGuard (src/payment/plan-limits/guards/plan-limit.guard.ts) calls checkEnterpriseAccessBlocked first (same as before), then probes planLimitsService.getCompanyPlanLimits(companyId)?.billingMode and early-returns for POSTPAID before invoking checkLimit. This is a defense-in-depth safeguard: POSTPAID plans are supposed to carry -1 for every limit by convention, but if a POSTPAID plan accidentally has a finite limit in DB, the guard still won't rate-limit the company.
Access Blocking When Billing Is Disabled
Even when BILLING_ENABLED=false (which skips all PREPAID billing checks), BillingGate.pre_check() explicitly checks for blocked enterprise scopes via the _check_enterprise_blocked() helper:
# _check_enterprise_blocked(company_id, user_id) delegates to SubscriptionModel:
if company_id:
if await SubscriptionModel.has_unpaid_postpaid_subscription(company_id):
raise SubscriptionInactiveError(company_id=company_id, status="UNPAID")
elif user_id:
if await SubscriptionModel.has_unpaid_postpaid_user_subscription(user_id):
raise SubscriptionInactiveError(company_id=None, status="UNPAID")
The company-scoped query joins Company → Subscription → Plan. The user-scoped query (has_unpaid_postpaid_user_subscription) filters directly on Subscription WHERE userId = $1 AND companyId IS NULL AND Plan.billingMode = 'POSTPAID' AND status = 'UNPAID'. This ensures that blocking any enterprise scope for non-payment is always enforced, regardless of the billing feature flag state.
This check only runs when no active subscription is found. If the scope has an active PREPAID subscription, the enterprise blocking check is skipped (a scope can only have one active subscription at a time).
Stripe Webhook Integration
invoice.paid Handler
When a Stripe invoice is paid, the webhook handler checks if it is an enterprise invoice:
- The event has a
stripeInvoiceIdbut nostripeSubscriptionId(enterprise invoices are standalone, not tied to a Stripe subscription). - The handler looks up the local
InvoicebystripeInvoiceId. - If the invoice has
billingPeriodStartset (the enterprise-invoice marker), the handler looks up the matchingUNPAIDPOSTPAID subscription: bycompanyIdwhen present, or by{userId: localInvoice.userId, companyId: null}for personal invoices. - If found, restores the subscription to
ACTIVEand emitsenterprise.access.restoredwithcompanyId: string | null.
This provides automatic access restoration for both company-scoped and personal enterprise subs when the billing owner pays via the Stripe-hosted payment link.
createUsageInvoice (Stripe Service)
The StripeService.createUsageInvoice() method:
- Gets or creates a Stripe customer for the billing owner.
- Creates a Stripe invoice with
collection_method: send_invoiceand the specifieddays_until_due. - Adds line items for each operation type (amount in cents).
- Sets metadata:
type: enterprise_usage,companyName. - Finalizes the invoice (triggers Stripe to send the payment email).
Notification Events
| Event | Trigger | Channel | Recipient |
|---|---|---|---|
enterprise.subscription.requested | User submits enterprise request | In-app | Super admins |
enterprise.subscription.rejected | Admin rejects request | In-app | Requesting user |
enterprise.invoice.generated | Monthly invoice created | Email + In-app | Billing owner |
enterprise.invoice.overdue | Invoice past due date | Email + In-app | Billing owner |
enterprise.access.blocked | Company access blocked | In-app | Billing owner |
enterprise.access.restored | Access restored (payment or admin) | In-app | Billing owner |
All notification handlers follow the fire-and-forget pattern (try-catch, never rethrow) and are implemented in BillingSystemNotificationListener. Event payload types accept companyId: string | null; listeners that show a human-readable scope label fall back to "A user (personal account)" (requester notifications) or the billing owner's email (UI labels) when the sub has no company.
Schema Changes
The enterprise billing system introduced the following schema additions:
New Enum: BillingMode
enum BillingMode {
PREPAID // Default: credits allocated upfront, consumed per operation
POSTPAID // Enterprise: unlimited access, invoiced monthly for AI usage
}
Plan Model
Added field:
billingMode BillingMode @default(PREPAID)
SubscriptionStatus Enum
Added values:
| Value | Purpose |
|---|---|
UNPAID | Enterprise subscription blocked for non-payment |
PENDING_APPROVAL | Enterprise request awaiting admin approval |
InvoiceStatus Enum
Added value:
| Value | Purpose |
|---|---|
OVERDUE | Enterprise invoice past its due date |
Invoice Model
Added fields:
| Field | Type | Description |
|---|---|---|
companyId | String? @db.Uuid | Enterprise company (null for personal-scope invoices) |
userId | String? @db.Uuid | Billing owner — set for every enterprise invoice, company-scoped or personal |
billingPeriodStart | DateTime? | Start of the billing period (presence identifies an enterprise invoice) |
billingPeriodEnd | DateTime? | End of the billing period |
dueDate | DateTime? | Payment deadline (5 days after generation) |
Added unique constraint:
@@unique([companyId, billingPeriodStart, billingPeriodEnd], name: "unique_enterprise_invoice_period")
Added index:
@@index([companyId, status])
Frontend Integration
The enterprise plan is admin-managed. There is no self-service checkout flow for enterprise companies.
Detecting Enterprise Subscriptions
Check the company's active plan for billingMode: POSTPAID:
query GetCompanySubscription($companyId: ID!) {
companySubscription(companyId: $companyId) {
id
status
isActive
Plan {
name
billingMode
}
}
}
If Plan.billingMode === "POSTPAID":
- Hide checkout and upgrade buttons -- enterprise companies do not buy credits or change plans through the UI.
- Hide credit balance display -- enterprise companies have no credit balance. Show "Enterprise" or "Unlimited" instead.
- Show usage dashboard -- display AI operation usage breakdown (see below).
- Show invoice history -- list invoices with status and payment links.
Handling Blocked State
If status === "UNPAID":
- Show a prominent banner indicating the company's access is blocked due to an overdue invoice.
- Link to the Stripe-hosted payment page (from the invoice's
stripeInvoiceUrl). - Disable all AI features (CV parsing, agent chat, matching, text rewrite).
Usage Breakdown (Admin View)
query EnterpriseUsage($companyId: ID!, $startDate: DateTime!, $endDate: DateTime!) {
adminEnterpriseUsageBreakdown(
companyId: $companyId
startDate: $startDate
endDate: $endDate
) {
companyName
lineItems {
operationType
operationCount
totalCost
totalInputTokens
totalOutputTokens
}
totalAmount
currency
}
}
Admin Panel Integration
The admin panel Enterprise tab (components/admin/payments/EnterpriseTab.tsx) surfaces:
- Subscription list — all enterprise subscriptions across all statuses, from
adminEnterpriseSubscriptions. Rows with a nullcompanyIdare labeledPersonal — <billing-owner-email>(no "Unnamed Company" fallback). - Pending requests — status chip
PENDING_APPROVALwith approve/reject actions. - Create subscription — Company ID field is optional; leave blank for a personal enterprise sub tied to the billing-owner user. Plan ID + Billing Owner User ID are required.
- Usage preview —
EnterpriseUsageModalacceptscompanyIdoruserIdvia the parent's row scope; it passes either through toadminEnterpriseUsageBreakdown. - On-demand invoice — Generate Invoice modal exposes both Company ID and Billing Owner User ID (XOR). The submit handler strips empty fields so only one is sent.
- Access control — Block/Restore buttons build a
{ companyId | userId }scope viagetRowScope(row)and route throughadminBlockEnterpriseAccess/adminRestoreEnterpriseAccesswithEnterpriseAccessScopeInput. No morerow.companyId as stringcasts.
getCompanyName(row) in EnterpriseTab.tsx returns "Personal — " + subscriber.email when row.company is null, using the already-fetched subscribedBy. This makes the list view clearly distinguish personal enterprise subs from company-scoped ones without a separate filter.
Comparison: Prepaid vs Post-Paid
| Aspect | Prepaid (Standard) | Post-Paid (Enterprise) |
|---|---|---|
| Billing mode | PREPAID | POSTPAID |
| Payment timing | Before usage | After usage (monthly) |
| Credit balance | Required, checked before each operation | Not used, balance = infinity |
| Plan limits | Enforced per plan configuration | All set to -1 (unlimited) |
| Subscription creation | Self-service via Stripe Checkout | Admin-created or admin-approved request |
| Invoice generation | Automatic by Stripe (recurring) | Cron job on 1st of month |
| Access blocking | PAST_DUE (Stripe managed) | UNPAID (custom overdue check) |
BillingGate.pre_check() | Checks subscription + plan limits + credit balance | Checks subscription status only |
BillingGate.check_and_consume() | Deducts credits + logs to AIOperationLog | Logs to AIOperationLog only |
BILLING_ENABLED flag | Respects flag (skips checks when false) | Always enforces UNPAID blocking |