feat(billing): wire HTTP controllers to API handlers
This commit is contained in:
@@ -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
|
references (sale.order subscription flow, account.move, payment_stripe) before
|
||||||
implementing — do NOT code those internals from memory.
|
implementing — do NOT code those internals from memory.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import http
|
from odoo import http
|
||||||
@@ -31,44 +31,56 @@ class FusionBillingApi(http.Controller):
|
|||||||
auth = request.httprequest.headers.get("Authorization", "")
|
auth = request.httprequest.headers.get("Authorization", "")
|
||||||
if not auth.startswith("Bearer "):
|
if not auth.startswith("Bearer "):
|
||||||
return None
|
return None
|
||||||
raw = auth[7:].strip()
|
return request.env["fusion.billing.service"].sudo()._match_api_key(auth[7:].strip()) or None
|
||||||
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):
|
def _json(self, payload, status=200):
|
||||||
return request.make_json_response(payload, status=status)
|
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 ───────────────────────────────────────────────────────────
|
# ── routes ───────────────────────────────────────────────────────────
|
||||||
@http.route(f"{API_BASE}/health", type="http", auth="none", methods=["GET"], csrf=False)
|
@http.route(f"{API_BASE}/health", type="http", auth="none", methods=["GET"], csrf=False)
|
||||||
def health(self, **kw):
|
def health(self, **kw):
|
||||||
return self._json({"status": "ok", "service": "fusion_centralize_billing"})
|
return self._json({"status": "ok", "service": "fusion_centralize_billing"})
|
||||||
|
|
||||||
@http.route(f"{API_BASE}/usage", type="http", auth="none", methods=["POST"], csrf=False)
|
@http.route(f"{API_BASE}/customers", type="http", auth="none", methods=["POST"], csrf=False)
|
||||||
def post_usage(self, **kw):
|
def post_customer(self, **kw):
|
||||||
"""Hot path: batch aggregated usage counters. Returns 202 once implemented."""
|
|
||||||
service = self._authenticate()
|
service = self._authenticate()
|
||||||
if not service:
|
if not service:
|
||||||
return self._json({"error": "unauthorized"}, status=401)
|
return self._json({"error": "unauthorized"}, status=401)
|
||||||
# TODO(spec §6): idempotent upsert into fusion.billing.usage by idempotency_key.
|
payload = self._read_json()
|
||||||
return self._json({"error": "not_implemented"}, status=501)
|
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
|
@http.route(f"{API_BASE}/usage", type="http", auth="none", methods=["POST"], csrf=False)
|
||||||
# self._authenticate():
|
def post_usage(self, **kw):
|
||||||
# POST /customers upsert res.partner + account.link
|
service = self._authenticate()
|
||||||
# POST /subscriptions create subscription sale.order
|
if not service:
|
||||||
# PUT /subscriptions/<id> change / upgrade
|
return self._json({"error": "unauthorized"}, status=401)
|
||||||
# DELETE /subscriptions/<id> cancel
|
payload = self._read_json()
|
||||||
# POST /invoices one-off invoice (token pack, throttle-removal)
|
if payload is None:
|
||||||
# GET /invoices list (filter by external customer)
|
return self._json({"error": "invalid json"}, status=400)
|
||||||
# GET /invoices/<id> fetch
|
return self._json(service._api_record_usage(payload), status=202)
|
||||||
# POST /invoices/<id>/download PDF
|
|
||||||
# POST /invoices/<id>/retry_payment retry
|
@http.route(f"{API_BASE}/plans", type="http", auth="none", methods=["GET"], csrf=False)
|
||||||
# POST /invoices/<id>/void void
|
def get_plans(self, **kw):
|
||||||
# POST /credit_notes refund (account.move reversal)
|
service = self._authenticate()
|
||||||
# GET /plans catalog/pricing for the app
|
if not service:
|
||||||
# POST /customers/<id>/checkout_url Stripe payment-method setup
|
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user