From c44fd89ed1e846b6429db9ca64c8fc8bc1c470d9 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 03:05:55 -0400 Subject: [PATCH] feat(billing): wire HTTP controllers to API handlers --- fusion_centralize_billing/controllers/api.py | 70 ++++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/fusion_centralize_billing/controllers/api.py b/fusion_centralize_billing/controllers/api.py index d9001684..5de832ea 100644 --- a/fusion_centralize_billing/controllers/api.py +++ b/fusion_centralize_billing/controllers/api.py @@ -12,7 +12,7 @@ to be implemented from the writing-plans output. Per repo CLAUDE.md, read live O references (sale.order subscription flow, account.move, payment_stripe) before implementing — do NOT code those internals from memory. """ -import hashlib +import json import logging from odoo import http @@ -31,44 +31,56 @@ class FusionBillingApi(http.Controller): 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 + 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}/usage", type="http", auth="none", methods=["POST"], csrf=False) - def post_usage(self, **kw): - """Hot path: batch aggregated usage counters. Returns 202 once implemented.""" + @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) - # TODO(spec §6): idempotent upsert into fusion.billing.usage by idempotency_key. - return self._json({"error": "not_implemented"}, status=501) + payload = self._read_json() + if payload is None: + return self._json({"error": "invalid json"}, status=400) + return self._json(service._api_upsert_customer(payload)) - # 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 + @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) + return self._json(service._api_record_usage(payload), 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) + return self._json(service._api_create_subscription(payload))