"""Billing API routes — Spec 023.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from app import auth, billing_store from app.permissions import can_manage_billing, can_read_billing, should_mask_sensitive router = APIRouter(prefix="/api/v1/billing", tags=["billing"]) class PatchBillingBody(BaseModel): billing_state: str | None = None recurrence_active: bool | None = None external_customer_id: str | None = None plan_code: str | None = None activated_by: str | None = None def _reader(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser: if not can_read_billing(user.role): raise HTTPException(403, "permissão insuficiente") return user def _manager(user: auth.DeskUser = Depends(auth.get_current_user)) -> auth.DeskUser: if not can_manage_billing(user.role): raise HTTPException(403, "permissão insuficiente") return user @router.get("/summary") def billing_summary(user: auth.DeskUser = Depends(_reader)): conn = auth.db() try: data = billing_store.summary(conn) if should_mask_sensitive(user.role): data["recent_validations"] = [ billing_store._row_dict(r, mask=True) for r in conn.execute( "SELECT * FROM billing_accounts ORDER BY updated_at DESC LIMIT 5" ).fetchall() ] return data finally: conn.close() @router.get("/accounts") def list_billing_accounts( billing_state: str = "", domain: str = "", limit: int = Query(100, ge=1, le=500), user: auth.DeskUser = Depends(_reader), ): conn = auth.db() try: mask = should_mask_sensitive(user.role) return billing_store.list_accounts( conn, billing_state=billing_state.strip() or None, domain=domain.strip() or None, limit=limit, mask=mask, ) finally: conn.close() @router.get("/accounts/by-domain/{domain}") def billing_by_domain(domain: str, user: auth.DeskUser = Depends(_reader)): conn = auth.db() try: acc = billing_store.get_by_domain(conn, domain, mask=should_mask_sensitive(user.role)) finally: conn.close() if not acc: raise HTTPException(404, "conta não encontrada") return acc @router.get("/accounts/{account_id}") def get_billing_account(account_id: int, user: auth.DeskUser = Depends(_reader)): conn = auth.db() try: acc = billing_store.get_account(conn, account_id, mask=should_mask_sensitive(user.role)) finally: conn.close() if not acc: raise HTTPException(404, "conta não encontrada") return acc @router.get("/accounts/{account_id}/activation-preview") def billing_activation_preview( account_id: int, plan_code: str = "", user: auth.DeskUser = Depends(_manager), ): """Preview read-only — Spec 043 Fase 1.""" from app.activation_mapper import build_activation_preview conn = auth.db() try: acc = billing_store.get_account(conn, account_id, mask=False) finally: conn.close() if not acc: raise HTTPException(404, "conta não encontrada") preview = build_activation_preview( acc, plan_code=plan_code.strip() or None, ) if should_mask_sensitive(user.role): preview["company_profile"] = billing_store._mask_profile(preview.get("company_profile") or {}) for key in ("email", "company_vat", "phone"): if preview["mapped"]["foss_client"].get(key): if "email" in key: preview["mapped"]["foss_client"][key] = billing_store._mask_email( preview["mapped"]["foss_client"][key] ) elif key == "company_vat": preview["mapped"]["foss_client"][key] = billing_store._mask_tax_id( preview["mapped"]["foss_client"][key] ) if preview["mapped"]["odoo_partner"].get("email"): preview["mapped"]["odoo_partner"]["email"] = billing_store._mask_email( preview["mapped"]["odoo_partner"]["email"] ) if preview["mapped"]["odoo_partner"].get("vat"): preview["mapped"]["odoo_partner"]["vat"] = billing_store._mask_tax_id( preview["mapped"]["odoo_partner"]["vat"] ) return preview @router.patch("/accounts/{account_id}") def patch_billing_account( account_id: int, body: PatchBillingBody, user: auth.DeskUser = Depends(_manager), ): conn = auth.db() try: fields = body.model_dump(exclude_none=True) if body.recurrence_active and not fields.get("activated_by"): from datetime import datetime, timezone fields["activated_by"] = user.username fields["activated_at"] = datetime.now(timezone.utc).isoformat() acc = billing_store.patch_account(conn, account_id, **fields) finally: conn.close() if not acc: raise HTTPException(404, "conta não encontrada") return acc