AI_INTEGRATION — billing-service
Conforms to the platform AI architecture in 08 AI Architecture. All inference runs in
ai-orchestrator-service;billing-servicecalls it through theAIClientport and persistsAIProvenanceon every AI-influenced row. AI is assistive, never authoritative. No domain invariant relies on an AI verdict; AI surfaces signals that humans review.
1. Capabilities owned by billing-service
| Capability | Trigger | Output | HITL gate |
|---|---|---|---|
| Folio anomaly detection | every folio.charge_added.v1, folio.payment_recorded.v1, folio.refund_recorded.v1 for an open folio | risk score + signal type | suggestion only — flagged in supervisor inbox if score > tenant threshold |
| Cash variance pattern detection | every cash_drawer.closed.v1 and the daily reconciliation job | per-actor and per-drawer drift trend | review by GM on threshold breach |
| Suspected fraud on dispute pattern | sequence of credit_note.generated.v1 against the same customer / agent | suspicion narrative + supporting evidence | review by finance, no auto-action |
| Subscription cancellation risk | monthly batch on subscriptions rows with state ∈ {grace, past_due} | churn probability score + drivers | informs platform CS outreach (no billing change) |
The AI never:
- approves a refund;
- waives a charge;
- changes a tax rate;
- closes a folio;
- closes a cash drawer;
- suspends or reactivates a subscription;
- generates an invoice number or PDF.
2. Folio anomaly detection — detailed flow
2.1 Trigger
Each folio mutation in Folio.postCharge, recordPayment, recordRefund enqueues an evaluation job into the local Pub/Sub topic melmastoon.billing.ai.folio_evaluation.requested.v1 (intra-service, not part of the public event catalog). A background consumer processes the job within p95 ≤ 5 s.
2.2 Inputs (PII-minimized)
interface FolioAnomalyInput {
tenantId: TenantId;
folioId: FolioId;
propertyId: PropertyId;
currency: ISO4217;
// last 50 mutations on this folio:
mutations: Array<{
kind: 'charge'|'payment'|'refund';
chargeKind?: ChargeKind;
amountMicro: string;
actorId: string; // hashed before send
postedAt: string; // ISO
source: { kind: string; ref?: string };
}>;
reservationContext: {
nightsBooked: number; partySize: number; rateClass: string; corporateAccountId?: string;
};
// tenant-level baselines (precomputed; no PII):
baselines: { medianFolioTotalMicro: string; meanRefundsPerFolio: number; perChargeKindMean: Record<ChargeKind, string> };
}
actorId is hashed (HMAC-SHA256 with a per-tenant key kept in Secret Manager) before transit so the model surface is unable to cross-correlate staff identities across tenants.
2.3 AI call
const result = await this.aiClient.classify({
capability: 'billing.folio_anomaly.v1',
tenantId, // routes to per-tenant model variant if any
input: folioAnomalyInput,
budgetMs: 1500,
fallback: { score: 0, signals: [] },
});
AIClient enforces:
- per-tenant rate limit (
tenant.settings.ai.budget.billing.qps); - circuit breaker (open after 10 consecutive failures, half-open after 60 s);
- structured prompt + JSON-schema-validated response (
zodschema rejects malformed output).
2.4 Output
interface FolioAnomalyResult {
score: number; // 0..1
signals: Array<{
type: 'unusual_charge_amount'|'rapid_charge_burst'|'refund_far_exceeds_norm'|'split_payment_evasion'|'after_hours_post'|'cross_actor_velocity';
severity: 'low'|'medium'|'high';
evidence: { mutationIds: string[]; explanation: string; deltaPct?: number };
}>;
modelVersion: string; // e.g., 'fold-anomaly-2026-04'
modelLatencyMs: number;
promptTokens: number;
completionTokens: number;
}
2.5 Persistence + provenance
Every signal above the tenant threshold (tenant.settings.ai.billing.folio.signalThreshold, default 0.7) writes a row in folio_ai_signals (per-tenant schema):
CREATE TABLE folio_ai_signals (
id text PRIMARY KEY, -- 'fas_…'
tenant_id text NOT NULL,
folio_id text NOT NULL REFERENCES folios(id),
signal_type text NOT NULL,
severity text NOT NULL,
score numeric(4,3) NOT NULL,
evidence jsonb NOT NULL,
reviewed_by text,
reviewed_at timestamptz,
resolution text CHECK (resolution IN (NULL,'confirmed','dismissed','escalated')),
ai_provenance jsonb NOT NULL, -- { capability, modelVersion, modelLatencyMs, promptTokens, completionTokens, traceId }
created_at timestamptz NOT NULL DEFAULT now()
);
The supervisor inbox query is:
SELECT * FROM folio_ai_signals WHERE resolution IS NULL ORDER BY severity DESC, created_at DESC LIMIT 100;
2.6 Human review
The desktop "Folio Risk" panel (visible to roles with billing.ai.review) shows pending signals, lets the reviewer click into the folio with the cited mutations highlighted, and records a confirmed/dismissed/escalated resolution. escalated posts to notification-service with the GM, the property owner, and the platform finance ops alias.
3. Cash variance pattern detection
Runs nightly in the billing-cash-analytics-job Cloud Run Job:
- Pull the last 30 days of
cash_drawer_sessionsper tenant + property. - Compute per-actor and per-drawer variance trends.
- Call
ai.classify({ capability: 'billing.cash_pattern.v1', input: { sessions } }). - Persist
cash_pattern_signalsrows withAIProvenance. - Notify the GM on
severity=high.
This capability does not retroactively change any session record.
4. Suspected fraud on dispute pattern
Triggered when a customer (matched by hashed email + phone fingerprint) accumulates ≥ 3 credit notes in 90 days across folios. The capability returns a narrative with the supporting evidence rows; finance reviews via the bff-platform-admin console.
5. Subscription churn risk
Monthly batch (subscription-churn-job) over subscriptions rows in state ∈ {grace, past_due}. Inputs include usage-record trend and dunning history (no PII; tenant-id only). Output feeds platform Customer Success — no billing state change.
6. AIProvenance contract (cross-cutting)
Every AI-influenced row stores AIProvenance:
interface AIProvenance {
capability: string; // 'billing.folio_anomaly.v1'
modelVersion: string; // returned by ai-orchestrator
callerVersion: string; // billing-service git sha
promptHash: string; // sha-256 of normalized prompt
inputHash: string; // sha-256 of normalized input
modelLatencyMs: number;
promptTokens: number;
completionTokens: number;
costMicro?: string; // amortized cost reported by ai-orchestrator
traceId: string;
createdAt: string;
}
This satisfies the platform-wide auditability requirement: every AI outcome is replayable from the inputs, the model version, and the prompt hash; no AI signal can appear in audit without a provenance row.
7. Per-tenant kill switch
tenant.settings.ai.billing.enabled = false disables all four capabilities for the tenant. The AIClient short-circuits with { score: 0, signals: [] }. The desktop hides the "Folio Risk" panel. Tenant admins manage this via bff-tenant-admin-service.
8. Data residency & PII
- Inputs are PII-minimized: actor IDs hashed; customer names, emails, and phones never sent.
- Inference runs in the same GCP region as the tenant's data bucket per 07 Security §8.
- The
ai-orchestrator-serviceenforces tenant-keyed routing to model variants; no cross-tenant model contamination.
9. Failure modes
| Failure | Behavior |
|---|---|
AIClient timeout > budgetMs | fallback {score:0, signals:[]}; metric billing_ai_timeout_total{capability} |
| Circuit open | identical fallback; alert if open > 10 min |
| Schema validation failure on response | drop signal; metric billing_ai_schema_invalid_total; do not retry |
ai-orchestrator returns BUDGET_EXHAUSTED | suppress capability for tenant for the cycle; surface notification to tenant admin |
10. Cross-references
- Platform AI architecture: 08 AI Architecture.
- AIClient contract & rate limits:
services/ai-orchestrator-service/API_CONTRACTS.md(when published). - Security expectations on hashed actor IDs: 07 Security §6.
- Notification routing for escalations:
notification-service(TBD bundle).