# Billing Logic — Whiteboard Version (for Devs) ## 1) Inputs You Always Need * `account_id`, `subscription_id` * `service_id` (or group → resolved to a service at dispatch) * `provider_id`, `asset_code` * `payload` (validated against service schema) * (Optional) `runner_id` * Idempotency key for the request (client-provided) --- ## 2) Gatekeeping (Hard Checks) 1. **Subscription** * Must be `active`. * Must target **exactly one** of {service, group}. * If group: ensure `service_id` is a member. 2. **Provider Allowlist** * If `subscription_providers` exists → `provider_id` must be listed. 3. **Spend Limit** (if set) * Compute window by `limit_period` (`hour`/`day`/`month`, UTC unless tenant TZ). * Current period spend = `SUM(ledger.amount WHERE account & currency & period)`. * `current_spend + estimated_charge ≤ limit_amount`. 4. **Max Duration** (effective; see §3): * If billing mode is `per_second`, reject if requested/max exceeds effective cap. --- ## 3) Effective Pricing (Single Resolution Function) Inputs: `provider_id`, `service_id`, `asset_code` Precedence: 1. `provider_service_overrides` for `(service_id, asset_code)` 2. `service_accepted_currencies` for `(service_id, asset_code)` 3. `services` defaults Outputs: * `effective_billing_mode ∈ {per_request, per_second}` * `effective_price` (NUMERIC) * `effective_max_request_seconds` (nullable) --- ## 4) Request Lifecycle (States) * `pending` → `running` → (`succeeded` | `failed` | `canceled`) * Timestamps: set `started_at` on `running`, `ended_at` on terminal states. * Enforce `ended_at ≥ started_at` and `duration ≤ effective_max_request_seconds` (if set). --- ## 5) Charging Rules ### A) Per Request ``` charge = effective_price ``` ### B) Per Second ``` duration_seconds = ceil(extract(epoch from (ended_at - started_at))) charge = duration_seconds * effective_price ``` * Cap with `effective_max_request_seconds` if present. * If ended early/failed before `started_at`: charge = 0. --- ## 6) Idempotency & Atomicity * **Idempotency key** per `(account_id, subscription_id, provider_id, service_id, request_external_id)`; store on `requests` and enforce unique index. * **Single transaction** to: 1. finalize `REQUESTS` status + timestamps, 2. insert **one** debit entry into `billing_ledger`. * Never mutate ledger entries; use compensating **credit** entries for adjustments/refunds. --- ## 7) Spend-Limit Enforcement (Before Charging) Pseudocode (SQL-ish): ```sql WITH window AS ( SELECT tsrange(period_start(:limit_period), period_end(:limit_period)) AS w ), spent AS ( SELECT COALESCE(SUM(amount), 0) AS total FROM billing_ledger, window WHERE account_id = :account_id AND asset_code = :asset_code AND created_at <@ (SELECT w FROM window) ), check AS ( SELECT (spent.total + :estimated_charge) <= :limit_amount AS ok FROM spent ) SELECT ok FROM check; ``` * If not ok → reject before dispatch, or allow but **set hard cap** on max seconds and auto-stop at limit. --- ## 8) Suggested DB Operations (Happy Path) 1. **Create request** ```sql INSERT INTO requests (...) VALUES (...) ON CONFLICT (idempotency_key) DO NOTHING RETURNING id; ``` 2. **Start execution** ```sql UPDATE requests SET status='running', started_at=now() WHERE id=:id AND status='pending'; ``` 3. **Finish & bill** (single transaction) ```sql BEGIN; -- lock for update to avoid double-billing UPDATE requests SET status=:final_status, ended_at=now() WHERE id=:id AND status='running' RETURNING started_at, ended_at; -- compute charge in app (see §5), re-check spend window here INSERT INTO billing_ledger ( account_id, provider_id, service_id, request_id, amount, asset_code, entry_type, description ) VALUES ( :account_id, :provider_id, :service_id, :id, :charge, :asset_code, 'debit', :desc ); COMMIT; ``` --- ## 9) Balances & Reporting * **Current balance** = `SUM(billing_ledger.amount) GROUP BY account_id, asset_code`. * Keep a **view** or **materialized view**; refresh asynchronously if needed. * Never rely on cached balance for hard checks — re-check within the billing transaction if **prepaid** semantics are required. --- ## 10) Error & Edge Rules * If runner fails before `running` → no charge. * If runner starts, then fails: * **per\_second**: bill actual seconds (can be 0). * **per\_request**: default is **no charge** unless policy says otherwise; if charging partials, document it. * Partial refunds/adjustments → insert **negative** ledger entries (type `credit`/`adjustment`) tied to the original `request_id`. --- ## 11) Minimal Pricing Resolver (Sketch) ```sql WITH p AS ( SELECT price_override AS price, billing_mode_override AS mode, max_request_seconds_override AS maxsec FROM provider_service_overrides WHERE provider_id = :pid AND service_id = :sid AND asset_code = :asset LIMIT 1 ), sac AS ( SELECT price_override AS price, billing_mode_override AS mode FROM service_accepted_currencies WHERE service_id = :sid AND asset_code = :asset LIMIT 1 ), svc AS ( SELECT default_price AS price, default_billing_mode AS mode, max_request_seconds AS maxsec FROM services WHERE id = :sid ) SELECT COALESCE(p.price, sac.price, svc.price) AS price, COALESCE(p.mode, sac.mode, svc.mode) AS mode, COALESCE(p.maxsec, svc.maxsec) AS max_seconds; ``` --- ## 12) Mermaid — Decision Trees ### Pricing & Duration ```mermaid flowchart TD A[provider_id, service_id, asset_code] --> B{Provider override exists?} B -- yes --> P[Use provider price/mode/max] B -- no --> C{Service currency override?} C -- yes --> S[Use service currency price/mode] C -- no --> D[Use service defaults] P --> OUT[effective price/mode/max] S --> OUT D --> OUT ``` ### Spend Check & Charge ```mermaid flowchart TD S[Has subscription limit?] -->|No| D1[Dispatch] S -->|Yes| C{current_spend + est_charge <= limit?} C -->|No| REJ[Reject or cap duration] C -->|Yes| D1[Dispatch] D1 --> RUN[Run request] RUN --> DONE[Finalize + insert ledger] ``` --- ## 13) Security Posture * Store **hash of subscription secret**; compare hash on use. * Sign client requests with **account pubkey**; verify before dispatch. * Limit **request schema** to validated fields; reject unknowns. * Enforce **IPv6** for runners where required. --- ## 14) What To Implement First 1. Pricing resolver (single function). 2. Spend-window checker (single query). 3. Request lifecycle + idempotency. 4. Ledger write (append-only) + balances view. Everything else layers on top. --- If you want, I can turn this into a small **README.md** with code blocks you can paste into the repo (plus a couple of SQL functions and example tests).