Enterprise Post-Paid Billing
Enterprise companies receive unlimited platform access without upfront payment. They are hand-picked, trusted organizations 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 company has 5 days to pay. If payment is not received, platform access is blocked until the invoice is settled.
Architecture
Admin creates subscription ──► Company gets unlimited access
│
AI operations run
(CV, chat, matching, etc.)
│
AIOperationLog entries
(no credit deduction)
│
┌─────────┴─────────┐
│ │
Monthly cron Daily cron
(1st at 3AM) (10AM)
│ │
Generate Stripe Check overdue
invoice + local invoices
record │
│ Block access
Send to if unpaid
billing owner
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 company, plan, and billing owner. The subscription is immediately set to ACTIVE with isActive: true. Any existing active subscriptions for the company are automatically canceled.
User-initiated (requires approval):
A company member calls requestEnterpriseSubscription. This 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 AI operation (CV extraction, agent chat, job parsing, text rewrite, meeting insight) 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 with a non-null
companyId. - For each company, queries
AIOperationLogfor the previous calendar month (grouped byoperationType). - Calculates the total cost from
creditsCostfields on successful operations. - If total usage is zero, skips invoice generation for that company.
- Creates a Stripe invoice with
collection_method: send_invoiceanddays_until_due: 5. - Creates a local
Invoicerecord with the billing period and due date. - 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 invoices with status
PENDINGorFAILEDwheredueDatehas passed, and the associated company has a POSTPAID subscription. - Updates the invoice status to
OVERDUE. - Blocks the company: sets the subscription status to
UNPAIDandisActivetofalse. - Emits
enterprise.invoice.overdueandenterprise.access.blockedevents.
When a company 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 companies 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, but a local invoice withcompanyId), finds theUNPAIDPOSTPAID subscription, restores it toACTIVE, and emitsenterprise.access.restored. - Admin manual override --
adminMarkInvoicePaid(invoiceId)updates the invoice toPAIDand restores the subscription if it was blocked. - Admin direct restore --
adminRestoreEnterpriseAccess(companyId)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
User requests ──► PENDING_APPROVAL ──► Admin approves ──► ACTIVE
│ │
Admin rejects Invoice overdue
│ │
CANCELED UNPAID
(blocked)
│
Payment received
or admin restores
│
ACTIVE
(restored)
Admin-initiated flow skips PENDING_APPROVAL and goes directly to ACTIVE:
Admin creates ──► ACTIVE ──► Invoice overdue ──► UNPAID ──► Payment ──► 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. Cancels any existing active subscriptions for the company.
mutation {
adminCreateEnterpriseSubscription(input: {
companyId: "company-uuid"
planId: "enterprise-plan-uuid"
billingOwnerId: "user-uuid"
}) {
id
status
isActive
startDate
Plan { name billingMode }
}
}
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 company. Sets subscription to UNPAID with isActive: false.
mutation {
adminBlockEnterpriseAccess(companyId: "company-uuid") {
id
status
isActive
}
}
adminRestoreEnterpriseAccess
Manually restores access for a blocked enterprise company. Finds the UNPAID POSTPAID subscription and sets it back to ACTIVE.
mutation {
adminRestoreEnterpriseAccess(companyId: "company-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.
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
}
}
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 a company over a specific period. This is a preview of what the invoice would contain.
query {
adminEnterpriseUsageBreakdown(
companyId: "company-uuid"
startDate: "2026-01-01T00:00:00Z"
endDate: "2026-01-31T23:59:59Z"
) {
companyId
companyName
periodStart
periodEnd
lineItems {
operationType
operationCount
totalCost
totalInputTokens
totalOutputTokens
}
totalAmount
currency
}
}
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 with the same
companyId,billingPeriodStart, andbillingPeriodEnd. - Database level -- a unique constraint
@@unique([companyId, billingPeriodStart, billingPeriodEnd])on theInvoicetable catches any concurrent generation attempts (Prisma error P2002).
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:
pre_check()
# Simplified flow for POSTPAID companies:
subscription = await SubscriptionModel.get_active_subscription(company_id)
billing_mode = subscription.get("billingMode", "PREPAID")
if billing_mode == "POSTPAID":
# Active subscription found -- allow immediately
# No plan limit check, no credit balance check
return True
For blocked companies (subscription status UNPAID), the get_active_subscription() query returns None because it filters by status IN ('ACTIVE', 'TRIALING'). The pre-check then looks for an UNPAID POSTPAID subscription 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.
Access Blocking When Billing Is Disabled
Even when BILLING_ENABLED=false (which skips all PREPAID billing checks), the BillingGate.pre_check() explicitly checks for blocked enterprise companies:
if not is_billing_enabled():
# Still check for blocked POSTPAID companies
blocked = await conn.fetchrow("""
SELECT s.id FROM "Company" c
JOIN "Subscription" s ON ...
JOIN "Plan" p ON ...
WHERE c.id = $1
AND p."billingMode" = 'POSTPAID'
AND s.status = 'UNPAID'
""", company_id)
if blocked:
raise SubscriptionInactiveError(status="UNPAID")
This ensures that blocking an enterprise company for non-payment is always enforced, regardless of the billing feature flag state.
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 a
companyId, it checks for anUNPAIDPOSTPAID subscription for that company. - If found, restores the subscription to
ACTIVEand emitsenterprise.access.restored.
This provides automatic access restoration 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.
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 | Links invoice to the enterprise company |
billingPeriodStart | DateTime? | Start of the billing period |
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 should include an Enterprise Subscriptions section with:
- Subscription list -- all enterprise subscriptions across all statuses, from
adminEnterpriseSubscriptions. - Pending requests -- filter by
status: PENDING_APPROVALwith approve/reject actions. - Create subscription -- form with company selector, plan selector (filtered to POSTPAID plans), and billing owner selector.
- Usage preview -- per-company usage breakdown with date range picker.
- On-demand invoice -- generate an invoice for a custom date range.
- Access control -- block/restore buttons for each active enterprise company.
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 |