Skip to main content

AI_INTEGRATION — billing-service

Conforms to the platform AI architecture in 08 AI Architecture. All inference runs in ai-orchestrator-service; billing-service calls it through the AIClient port and persists AIProvenance on 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

CapabilityTriggerOutputHITL gate
Folio anomaly detectionevery folio.charge_added.v1, folio.payment_recorded.v1, folio.refund_recorded.v1 for an open foliorisk score + signal typesuggestion only — flagged in supervisor inbox if score > tenant threshold
Cash variance pattern detectionevery cash_drawer.closed.v1 and the daily reconciliation jobper-actor and per-drawer drift trendreview by GM on threshold breach
Suspected fraud on dispute patternsequence of credit_note.generated.v1 against the same customer / agentsuspicion narrative + supporting evidencereview by finance, no auto-action
Subscription cancellation riskmonthly batch on subscriptions rows with state ∈ {grace, past_due}churn probability score + driversinforms 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 (zod schema 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:

  1. Pull the last 30 days of cash_drawer_sessions per tenant + property.
  2. Compute per-actor and per-drawer variance trends.
  3. Call ai.classify({ capability: 'billing.cash_pattern.v1', input: { sessions } }).
  4. Persist cash_pattern_signals rows with AIProvenance.
  5. 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-service enforces tenant-keyed routing to model variants; no cross-tenant model contamination.

9. Failure modes

FailureBehavior
AIClient timeout > budgetMsfallback {score:0, signals:[]}; metric billing_ai_timeout_total{capability}
Circuit openidentical fallback; alert if open > 10 min
Schema validation failure on responsedrop signal; metric billing_ai_schema_invalid_total; do not retry
ai-orchestrator returns BUDGET_EXHAUSTEDsuppress 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).