# -*- 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 json 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 return request.env["fusion.billing.service"].sudo()._match_api_key(auth[7:].strip()) or None def _json(self, payload, status=200): return request.make_json_response(payload, status=status) def _read_json(self): try: raw = request.httprequest.get_data(as_text=True) or "{}" return json.loads(raw) except Exception: return None # ── 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}/customers", type="http", auth="none", methods=["POST"], csrf=False) def post_customer(self, **kw): service = self._authenticate() if not service: return self._json({"error": "unauthorized"}, status=401) payload = self._read_json() if payload is None: return self._json({"error": "invalid json"}, status=400) result = service._api_upsert_customer(payload) if result.get("status") == "error": return self._json(result, status=400) return self._json(result) @http.route(f"{API_BASE}/usage", type="http", auth="none", methods=["POST"], csrf=False) def post_usage(self, **kw): service = self._authenticate() if not service: return self._json({"error": "unauthorized"}, status=401) payload = self._read_json() if payload is None: return self._json({"error": "invalid json"}, status=400) result = service._api_record_usage(payload) if result.get("status") == "error": return self._json(result, status=400) return self._json(result, status=202) @http.route(f"{API_BASE}/plans", type="http", auth="none", methods=["GET"], csrf=False) def get_plans(self, **kw): service = self._authenticate() if not service: return self._json({"error": "unauthorized"}, status=401) return self._json(service._api_catalog()) @http.route(f"{API_BASE}/subscriptions", type="http", auth="none", methods=["POST"], csrf=False) def post_subscription(self, **kw): service = self._authenticate() if not service: return self._json({"error": "unauthorized"}, status=401) payload = self._read_json() if payload is None: return self._json({"error": "invalid json"}, status=400) result = service._api_create_subscription(payload) if result.get("status") == "error": return self._json(result, status=400) return self._json(result)