# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 """Inbound, Lago-shaped billing API (spec §7). Auth: bearer API key matched (by SHA-256 hash) against ``fusion.billing.service``. Routing: ``type="http"`` + ``auth="none"`` + ``csrf=False`` — external apps present bearer tokens, not Odoo sessions (so NOT ``type="jsonrpc"``). STATUS: SCAFFOLD. Only auth + /health are wired. Endpoint bodies are stubs (HTTP 501) to be implemented from the writing-plans output. Per repo CLAUDE.md, read live Odoo 19 references (sale.order subscription flow, account.move, payment_stripe) before implementing — do NOT code those internals from memory. """ import hashlib import logging from odoo import http from odoo.http import request _logger = logging.getLogger(__name__) API_BASE = "/api/billing/v1" class FusionBillingApi(http.Controller): # ── helpers ────────────────────────────────────────────────────────── def _authenticate(self): """Return the active fusion.billing.service for the bearer key, else None.""" auth = request.httprequest.headers.get("Authorization", "") if not auth.startswith("Bearer "): return None raw = auth[7:].strip() if not raw: return None key_hash = hashlib.sha256(raw.encode()).hexdigest() service = request.env["fusion.billing.service"].sudo().search( [("api_key_hash", "=", key_hash), ("active", "=", True)], limit=1, ) return service or None def _json(self, payload, status=200): return request.make_json_response(payload, status=status) # ── routes ─────────────────────────────────────────────────────────── @http.route(f"{API_BASE}/health", type="http", auth="none", methods=["GET"], csrf=False) def health(self, **kw): return self._json({"status": "ok", "service": "fusion_centralize_billing"}) @http.route(f"{API_BASE}/usage", type="http", auth="none", methods=["POST"], csrf=False) def post_usage(self, **kw): """Hot path: batch aggregated usage counters. Returns 202 once implemented.""" service = self._authenticate() if not service: return self._json({"error": "unauthorized"}, status=401) # TODO(spec §6): idempotent upsert into fusion.billing.usage by idempotency_key. return self._json({"error": "not_implemented"}, status=501) # TODO(spec §7): implement the remaining Lago-shaped endpoints, each gated by # self._authenticate(): # POST /customers upsert res.partner + account.link # POST /subscriptions create subscription sale.order # PUT /subscriptions/ change / upgrade # DELETE /subscriptions/ cancel # POST /invoices one-off invoice (token pack, throttle-removal) # GET /invoices list (filter by external customer) # GET /invoices/ fetch # POST /invoices//download PDF # POST /invoices//retry_payment retry # POST /invoices//void void # POST /credit_notes refund (account.move reversal) # GET /plans catalog/pricing for the app # POST /customers//checkout_url Stripe payment-method setup