Skip to main content

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:

  1. No credit balance -- enterprise companies bypass all credit checks. The BillingGate returns balance=infinity for POSTPAID subscriptions.
  2. Usage-based invoicing -- AI operations are logged to AIOperationLog without 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 creditsPerMonth allocation is needed (set to 0).
  • The BillingGate.pre_check() in background-tasks detects the POSTPAID billing mode and returns immediately with balance=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 AIOperationLog entry with metadata.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:

  1. Finds all active POSTPAID subscriptions with a non-null companyId.
  2. For each company, queries AIOperationLog for the previous calendar month (grouped by operationType).
  3. Calculates the total cost from creditsCost fields on successful operations.
  4. If total usage is zero, skips invoice generation for that company.
  5. Creates a Stripe invoice with collection_method: send_invoice and days_until_due: 5.
  6. Creates a local Invoice record with the billing period and due date.
  7. Emits the enterprise.invoice.generated event (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:

  1. Finds invoices with status PENDING or FAILED where dueDate has passed, and the associated company has a POSTPAID subscription.
  2. Updates the invoice status to OVERDUE.
  3. Blocks the company: sets the subscription status to UNPAID and isActive to false.
  4. Emits enterprise.invoice.overdue and enterprise.access.blocked events.

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.paid webhook fires. The handler detects the enterprise invoice (no Stripe subscription ID, but a local invoice with companyId), finds the UNPAID POSTPAID subscription, restores it to ACTIVE, and emits enterprise.access.restored.
  • Admin manual override -- adminMarkInvoicePaid(invoiceId) updates the invoice to PAID and 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:

FieldValueDescription
nameEnterprisePlan display name
price0No recurring charge (usage-based)
billingModePOSTPAIDDistinguishes from standard prepaid plans
creditsPerMonth0No upfront credit allocation
All 16 feature limits-1Unlimited access to all features
trialDays0No trial period (admin-managed)
info

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

StatusisActiveMeaning
PENDING_APPROVALfalseUser requested, awaiting admin approval
ACTIVEtrueFull platform access, AI operations logged for invoicing
UNPAIDfalseInvoice overdue, all platform features blocked
CANCELEDfalseRejected 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
}
}
warning

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

JobScheduleDescription
Invoice generation0 3 1 * * (1st of month, 3 AM UTC)Generates invoices for previous month's usage
Overdue check0 10 * * * (daily, 10 AM UTC)Blocks companies with past-due invoices

Invoice Creation Process

  1. Calculate the previous month's period: periodStart is the 1st at 00:00 UTC, periodEnd is the last day at 23:59:59.999 UTC.
  2. Query AIOperationLog with groupBy(['operationType']), filtering by companyId, date range, and status: SUCCESS.
  3. Sum creditsCost across all operation types for the total invoice amount.
  4. If total is zero (no billable usage), skip invoice generation.
  5. Look up the billing owner from the subscription (subscribedById or userId).
  6. Create Stripe invoice line items -- one per operation type, with the format "CV Extraction -- 42 operations".
  7. Create a Stripe invoice with collection_method: send_invoice and days_until_due: 5. Stripe metadata includes type: enterprise_usage and the company name.
  8. Finalize and send the Stripe invoice (Stripe emails the billing owner with a payment link).
  9. Create a local Invoice record with PENDING status, 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, and billingPeriodEnd.
  • Database level -- a unique constraint @@unique([companyId, billingPeriodStart, billingPeriodEnd]) on the Invoice table 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 adminMarkInvoicePaid for 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 AIOperationLog entry with credits_cost set to the calculated amount.
  • Adds billing_mode: "POSTPAID" to the metadata JSON.
  • Does not touch CreditBalance or CreditTransaction tables.
  • Returns balance_after = float("inf").
caution

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:

  1. The event has a stripeInvoiceId but no stripeSubscriptionId (enterprise invoices are standalone, not tied to a Stripe subscription).
  2. The handler looks up the local Invoice by stripeInvoiceId.
  3. If the invoice has a companyId, it checks for an UNPAID POSTPAID subscription for that company.
  4. If found, restores the subscription to ACTIVE and emits enterprise.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:

  1. Gets or creates a Stripe customer for the billing owner.
  2. Creates a Stripe invoice with collection_method: send_invoice and the specified days_until_due.
  3. Adds line items for each operation type (amount in cents).
  4. Sets metadata: type: enterprise_usage, companyName.
  5. Finalizes the invoice (triggers Stripe to send the payment email).

Notification Events

EventTriggerChannelRecipient
enterprise.subscription.requestedUser submits enterprise requestIn-appSuper admins
enterprise.subscription.rejectedAdmin rejects requestIn-appRequesting user
enterprise.invoice.generatedMonthly invoice createdEmail + In-appBilling owner
enterprise.invoice.overdueInvoice past due dateEmail + In-appBilling owner
enterprise.access.blockedCompany access blockedIn-appBilling owner
enterprise.access.restoredAccess restored (payment or admin)In-appBilling 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:

ValuePurpose
UNPAIDEnterprise subscription blocked for non-payment
PENDING_APPROVALEnterprise request awaiting admin approval

InvoiceStatus Enum

Added value:

ValuePurpose
OVERDUEEnterprise invoice past its due date

Invoice Model

Added fields:

FieldTypeDescription
companyIdString? @db.UuidLinks invoice to the enterprise company
billingPeriodStartDateTime?Start of the billing period
billingPeriodEndDateTime?End of the billing period
dueDateDateTime?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

info

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:

  1. Subscription list -- all enterprise subscriptions across all statuses, from adminEnterpriseSubscriptions.
  2. Pending requests -- filter by status: PENDING_APPROVAL with approve/reject actions.
  3. Create subscription -- form with company selector, plan selector (filtered to POSTPAID plans), and billing owner selector.
  4. Usage preview -- per-company usage breakdown with date range picker.
  5. On-demand invoice -- generate an invoice for a custom date range.
  6. Access control -- block/restore buttons for each active enterprise company.

Comparison: Prepaid vs Post-Paid

AspectPrepaid (Standard)Post-Paid (Enterprise)
Billing modePREPAIDPOSTPAID
Payment timingBefore usageAfter usage (monthly)
Credit balanceRequired, checked before each operationNot used, balance = infinity
Plan limitsEnforced per plan configurationAll set to -1 (unlimited)
Subscription creationSelf-service via Stripe CheckoutAdmin-created or admin-approved request
Invoice generationAutomatic by Stripe (recurring)Cron job on 1st of month
Access blockingPAST_DUE (Stripe managed)UNPAID (custom overdue check)
BillingGate.pre_check()Checks subscription + plan limits + credit balanceChecks subscription status only
BillingGate.check_and_consume()Deducts credits + logs to AIOperationLogLogs to AIOperationLog only
BILLING_ENABLED flagRespects flag (skips checks when false)Always enforces UNPAID blocking