Billing
Import these functions and types from @antelopejs/interface-dms-saas/billing or the package root.
Read a customer's balance
Use this read when a billing page needs to distinguish credit from money owed. Pass IDs obtained from your authenticated request, not unchecked request fields.
import {
GetTenantCustomerBalance,
type TenantCustomerBalanceScope,
} from "@antelopejs/interface-dms-saas/billing";
export async function describeBalance(scope: TenantCustomerBalanceScope): Promise<string> {
const balance = await GetTenantCustomerBalance(scope);
if (balance.status === "absent") return `No balance: ${balance.reason}`;
const amount = balance.balanceMinorUnits;
if (amount === 0) return "Settled";
return `${Math.abs(amount)} ${balance.currency} minor units ${amount < 0 ? "in credit" : "owed"}`;
}
For an available EUR balance of -1250, this returns 1250 EUR minor units in credit: €12.50 available for future invoices, not a refund. A provider failure
rejects the call; do not turn it into a zero balance. The reference below covers
all absence reasons and the separate upcoming-invoice estimate.
GetTenantCustomerBalance
GetTenantCustomerBalance(scope: TenantCustomerBalanceScope): Promise<TenantCustomerBalance> takes { tenantId: string, userId: string } from a trusted authentication boundary. It requires membership and a successful tenant access gate; it rejects unauthorized callers with 403. It does not trust a supplied user ID as proof of authentication.
| Result | Fields |
|---|---|
AvailableTenantCustomerBalance | status: "available", balanceMinorUnits: number, currency: string |
AbsentTenantCustomerBalance | status: "absent", reason: TenantCustomerBalanceAbsenceReason, balanceMinorUnits: null, currency: null |
Amounts are Stripe minor units: negative means customer credit, positive means money owed, and zero means settled. Currency is uppercase ISO 4217. Absence reasons are complimentary, customer_not_configured, customer_deleted, and currency_unavailable. Stripe read failures reject with 502 and saas.errors.billing.customer_balance_provider_unavailable; they are not an absent balance. This call does not charge or refund the customer.
isComplimentarySubscription(subscription: ComplimentarySubscriptionState | null | undefined): boolean is a synchronous helper. ComplimentarySubscriptionState has isComplimentary?: boolean | null; only an explicit true returns true. A free plan alone is not the complimentary marker.
GetUpcomingInvoicePreview
GetUpcomingInvoicePreview(scope: UpcomingInvoicePreviewScope): Promise<UpcomingInvoicePreview> uses the same { tenantId, userId } scope and authorization checks. Stripe failures become an unavailable result rather than an invented total; authentication, implementation availability or local storage failures can still reject.
status | Result type and reason |
|---|---|
available | AvailableUpcomingInvoicePreview: Stripe priced the next invoice |
absent | AbsentUpcomingInvoicePreview: complimentary, free_plan, customer_not_configured, subscription_not_configured, or no_upcoming_invoice |
unavailable | UnavailableUpcomingInvoicePreview: stripe_not_configured, tax_not_configured, tax_location_invalid, tax_location_required, tax_calculation_failed, usage_unavailable, or provider_error |
Both non-available variants have reason and computedAt: string. The corresponding exported unions are UpcomingInvoicePreviewAbsenceReason and UpcomingInvoicePreviewUnavailableReason.
An available preview includes these fields. All amounts are integers in the minor units of its uppercase currency; all dates are ISO 8601 strings.
| Fields | Type and meaning |
|---|---|
subtotalMinorUnits, totalExcludingTaxMinorUnits, taxMinorUnits, totalMinorUnits, amountDueMinorUnits | number: subtotal before discounts/tax; total after discounts before tax; tax; tax-inclusive total; amount after customer balance |
lines, hasMoreLines | UpcomingInvoiceLine[], boolean: line page and truncation flag; totals remain complete |
taxes, taxCountry, isReverseCharge | UpcomingInvoiceTax[], string | null, boolean |
periodStart, periodEnd, billingDate, computedAt | string: cycle bounds, issue date and computation time |
usageThrough | string | null: end of quoted usage, null without usage lines |
UpcomingInvoiceLine has kind: UpcomingInvoiceLineKind (subscription, usage, or invoice_item), description: string | null, amountMinorUnits: number, taxMinorUnits: number, quantity: number | null, periodStart: string, periodEnd: string, isProration: boolean, usageLineKey: string | null, and productId: string | null. Usage keys are <provider id>:<key>; other line kinds have a null usage key.
UpcomingInvoiceTax has amountMinorUnits: number, taxableAmountMinorUnits: number | null, isInclusive: boolean, ratePercentage: number | null, isReverseCharge: boolean, and nullable string fields country, taxType, displayName, jurisdiction, and taxabilityReason. A reverse charge can produce zero tax with isReverseCharge: true.
The preview calls usage providers with isPreview: true and can populate a tenant-level shared database cache. It never creates an invoice or charges quoted lines. Usage covers the running cycle so far, not its final total; display computedAt. A missing payment method does not itself prevent previewing. Cache lifetime, invalidation and Stripe read timeouts are runtime configuration.
RecomputeTenantBillingState
RecomputeTenantBillingState(tenantId: string): Promise<void> republishes the tenant's billing state and workspace directory fields, including plan, seats, MRR, renewal and owner, from current records. Call it after a seed or import finishes direct writes. Normal runtime billing operations already do this.
This is a trusted administrative call with no user scope or membership check. Authorize the tenant before invoking it. It writes derived database state, does not mutate the Stripe subscription, and does not replace lifecycle delivery or preview-cache invalidation. Deleted directory rows and subscriptions whose deletion has started are left alone. Storage errors propagate; repeated concurrent-publication conflicts reject after bounded retries. Await completion and report failures instead of displaying stale directory state as current.
import { RecomputeTenantBillingState } from "@antelopejs/interface-dms-saas/billing";
// Invoke only after the import has committed all rows for these authorized tenants.
export async function refreshImportedTenants(tenantIds: readonly string[]): Promise<void> {
for (const tenantId of new Set(tenantIds)) {
await RecomputeTenantBillingState(tenantId);
}
}