| """ |
| AP/AR API Routes - Phase 41 |
| """ |
|
|
| from datetime import datetime |
| import io |
| import logging |
| from typing import Any, Dict, List, Optional |
| from fastapi import Depends, HTTPException |
| from fastapi.responses import StreamingResponse |
| from pydantic import BaseModel |
|
|
| from core.base_routes import BaseAPIRouter |
|
|
| router = BaseAPIRouter(prefix="/apar", tags=["AP/AR"]) |
|
|
| class APIntakeRequest(BaseModel): |
| vendor: str |
| amount: float |
| due_date: Optional[str] = None |
| line_items: List[Dict[str, Any]] = [] |
| payment_terms: str = "Net 30" |
| source: str = "email" |
|
|
| class ARGenerateRequest(BaseModel): |
| customer: str |
| amount: float |
| due_date: Optional[str] = None |
| line_items: List[Dict[str, Any]] = [] |
| source: str = "manual" |
|
|
| |
|
|
| @router.post("/ap/intake") |
| async def intake_ap_invoice(request: APIntakeRequest): |
| from core.apar_engine import apar_engine |
|
|
| data = { |
| "vendor": request.vendor, |
| "amount": request.amount, |
| "due_date": request.due_date, |
| "line_items": request.line_items, |
| "payment_terms": request.payment_terms |
| } |
|
|
| invoice = apar_engine.intake_invoice(request.source, data) |
|
|
| return router.success_response( |
| data={ |
| "id": invoice.id, |
| "vendor": invoice.vendor, |
| "amount": invoice.amount, |
| "status": invoice.status.value, |
| "auto_approved": invoice.approved_by == "auto" |
| }, |
| message="AP invoice intake successful" |
| ) |
|
|
| @router.post("/ap/{invoice_id}/approve") |
| async def approve_ap_invoice(invoice_id: str, approver: str = "user"): |
| from core.apar_engine import apar_engine |
|
|
| invoice = apar_engine.approve_invoice(invoice_id, approver) |
| return router.success_response( |
| data={"status": "approved", "id": invoice_id}, |
| message="Invoice approved successfully" |
| ) |
|
|
| @router.get("/ap/pending") |
| async def get_pending_approvals(): |
| from core.apar_engine import apar_engine |
|
|
| pending = apar_engine.get_pending_approvals() |
| return router.success_response( |
| data={ |
| "count": len(pending), |
| "invoices": [ |
| {"id": inv.id, "vendor": inv.vendor, "amount": inv.amount} |
| for inv in pending |
| ] |
| }, |
| message=f"Retrieved {len(pending)} pending approvals" |
| ) |
|
|
| @router.get("/ap/upcoming") |
| async def get_upcoming_payments(days: int = 7): |
| from core.apar_engine import apar_engine |
|
|
| upcoming = apar_engine.get_upcoming_payments(days) |
| return router.success_response( |
| data={ |
| "count": len(upcoming), |
| "total_due": sum(inv.amount for inv in upcoming), |
| "invoices": [ |
| {"id": inv.id, "vendor": inv.vendor, "amount": inv.amount, "due_date": inv.due_date.isoformat()} |
| for inv in upcoming |
| ] |
| }, |
| message=f"Retrieved {len(upcoming)} upcoming payments" |
| ) |
|
|
| |
|
|
| @router.post("/ar/generate") |
| async def generate_ar_invoice(request: ARGenerateRequest): |
| from core.apar_engine import apar_engine |
|
|
| data = { |
| "customer": request.customer, |
| "amount": request.amount, |
| "due_date": request.due_date, |
| "line_items": request.line_items |
| } |
|
|
| invoice = apar_engine.generate_invoice(request.source, data) |
|
|
| return router.success_response( |
| data={ |
| "id": invoice.id, |
| "customer": invoice.customer, |
| "amount": invoice.amount |
| }, |
| message="AR invoice generated successfully" |
| ) |
|
|
| @router.post("/ar/{invoice_id}/send") |
| async def send_ar_invoice(invoice_id: str): |
| from core.apar_engine import apar_engine |
|
|
| invoice = apar_engine.send_invoice(invoice_id) |
| return router.success_response( |
| data={"status": "sent", "id": invoice_id}, |
| message="Invoice sent successfully" |
| ) |
|
|
| @router.post("/ar/{invoice_id}/paid") |
| async def mark_ar_paid(invoice_id: str): |
| from core.apar_engine import apar_engine |
|
|
| invoice = apar_engine.mark_paid(invoice_id) |
| return router.success_response( |
| data={"status": "paid", "id": invoice_id}, |
| message="Invoice marked as paid" |
| ) |
|
|
| @router.get("/ar/overdue") |
| async def get_overdue_invoices(): |
| from core.apar_engine import apar_engine |
|
|
| overdue = apar_engine.get_overdue_invoices() |
| return router.success_response( |
| data={ |
| "count": len(overdue), |
| "invoices": [ |
| {"id": inv.id, "customer": inv.customer, "amount": inv.amount} |
| for inv in overdue |
| ] |
| }, |
| message=f"Retrieved {len(overdue)} overdue invoices" |
| ) |
|
|
| @router.get("/all") |
| async def get_all_invoices(): |
| """Get all invoices (AR and AP)""" |
| from core.apar_engine import apar_engine |
| |
| all_invoices = apar_engine.get_all_invoices() |
| |
| formatted_invoices = [] |
| for inv in all_invoices: |
| |
| is_ar = hasattr(inv, "customer") |
| |
| formatted_invoices.append({ |
| "id": inv.id, |
| "customer": inv.customer if is_ar else None, |
| "vendor": getattr(inv, "vendor", None) if not is_ar else None, |
| "amount": inv.amount, |
| "due_date": inv.due_date.isoformat(), |
| "status": inv.status.value, |
| "type": "AR" if is_ar else "AP" |
| }) |
|
|
| return router.success_response( |
| data={ |
| "count": len(formatted_invoices), |
| "invoices": formatted_invoices |
| }, |
| message=f"Retrieved {len(formatted_invoices)} invoices" |
| ) |
|
|
| @router.post("/ar/{invoice_id}/remind") |
| async def send_reminder(invoice_id: str): |
| from core.apar_engine import apar_engine |
|
|
| reminder = apar_engine.generate_reminder(invoice_id) |
| return router.success_response( |
| data=reminder, |
| message="Reminder generated successfully" |
| ) |
|
|
| @router.get("/summary") |
| async def get_collection_summary(): |
| from core.apar_engine import apar_engine |
| summary = apar_engine.get_collection_summary() |
| return router.success_response( |
| data=summary, |
| message="Collection summary retrieved successfully" |
| ) |
|
|
| @router.get("/ar/{invoice_id}/download") |
| async def download_ar_invoice(invoice_id: str): |
| from core.apar_engine import apar_engine |
| try: |
| pdf_bytes = apar_engine.generate_invoice_pdf(invoice_id) |
| file_obj = io.BytesIO(pdf_bytes) |
| return StreamingResponse( |
| file_obj, |
| media_type="application/pdf", |
| headers={"Content-Disposition": f"attachment; filename=invoice_{invoice_id}.pdf"} |
| ) |
| except ValueError as e: |
| raise HTTPException(status_code=404, detail=str(e)) |
| except ImportError as e: |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.get("/ap/{invoice_id}/download") |
| async def download_ap_invoice(invoice_id: str): |
| from core.apar_engine import apar_engine |
| try: |
| pdf_bytes = apar_engine.generate_invoice_pdf(invoice_id) |
| file_obj = io.BytesIO(pdf_bytes) |
| return StreamingResponse( |
| file_obj, |
| media_type="application/pdf", |
| headers={"Content-Disposition": f"attachment; filename=invoice_{invoice_id}.pdf"} |
| ) |
| except ValueError as e: |
| raise HTTPException(status_code=404, detail=str(e)) |
| except ImportError as e: |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|