Files
orion/app/modules/billing/routes/api/vendor_addons.py
Samir Boulahtit e2cecff014 refactor: migrate vendor billing, invoices, payments to module auto-discovery
Billing module:
- Create vendor_checkout.py (checkout, portal, cancel, reactivate, change-tier)
- Create vendor_addons.py (addon management routes)
- Update vendor.py to aggregate new routers

Orders module:
- Create vendor_invoices.py (invoice settings, CRUD, PDF generation)
- Update vendor.py to aggregate invoices router

Payments module:
- Restructure routes from routes/ to routes/api/
- Add require_module_access dependency
- Set is_self_contained=True for auto-discovery

Remove legacy files:
- app/api/v1/vendor/billing.py
- app/api/v1/vendor/invoices.py
- app/api/v1/vendor/payments.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 13:49:58 +01:00

180 lines
4.9 KiB
Python

# app/modules/billing/routes/api/vendor_addons.py
"""
Vendor add-on management endpoints.
Provides:
- List available add-ons
- Get vendor's purchased add-ons
- Purchase add-on
- Cancel add-on
All routes require module access control for the 'billing' module.
"""
import logging
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.config import settings
from app.core.database import get_db
from app.modules.billing.services import billing_service
from models.schema.auth import UserContext
vendor_addons_router = APIRouter(
prefix="/addons",
dependencies=[Depends(require_module_access("billing"))],
)
logger = logging.getLogger(__name__)
# ============================================================================
# Schemas
# ============================================================================
class AddOnResponse(BaseModel):
"""Add-on product information."""
id: int
code: str
name: str
description: str | None = None
category: str
price_cents: int
billing_period: str
quantity_unit: str | None = None
quantity_value: int | None = None
class VendorAddOnResponse(BaseModel):
"""Vendor's purchased add-on."""
id: int
addon_code: str
addon_name: str
status: str
domain_name: str | None = None
quantity: int
period_start: str | None = None
period_end: str | None = None
class AddOnPurchaseRequest(BaseModel):
"""Request to purchase an add-on."""
addon_code: str
domain_name: str | None = None # For domain add-ons
quantity: int = 1
class AddOnCancelResponse(BaseModel):
"""Response for add-on cancellation."""
message: str
addon_code: str
# ============================================================================
# Endpoints
# ============================================================================
@vendor_addons_router.get("", response_model=list[AddOnResponse])
def get_available_addons(
category: str | None = Query(None, description="Filter by category"),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Get available add-on products."""
addons = billing_service.get_available_addons(db, category=category)
return [
AddOnResponse(
id=addon.id,
code=addon.code,
name=addon.name,
description=addon.description,
category=addon.category,
price_cents=addon.price_cents,
billing_period=addon.billing_period,
quantity_unit=addon.quantity_unit,
quantity_value=addon.quantity_value,
)
for addon in addons
]
@vendor_addons_router.get("/my-addons", response_model=list[VendorAddOnResponse])
def get_vendor_addons(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Get vendor's purchased add-ons."""
vendor_id = current_user.token_vendor_id
vendor_addons = billing_service.get_vendor_addons(db, vendor_id)
return [
VendorAddOnResponse(
id=va.id,
addon_code=va.addon_product.code,
addon_name=va.addon_product.name,
status=va.status,
domain_name=va.domain_name,
quantity=va.quantity,
period_start=va.period_start.isoformat() if va.period_start else None,
period_end=va.period_end.isoformat() if va.period_end else None,
)
for va in vendor_addons
]
@vendor_addons_router.post("/purchase")
def purchase_addon(
request: AddOnPurchaseRequest,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Purchase an add-on product."""
vendor_id = current_user.token_vendor_id
vendor = billing_service.get_vendor(db, vendor_id)
# Build URLs
base_url = f"https://{settings.platform_domain}"
success_url = f"{base_url}/vendor/{vendor.vendor_code}/billing?addon_success=true"
cancel_url = f"{base_url}/vendor/{vendor.vendor_code}/billing?addon_cancelled=true"
result = billing_service.purchase_addon(
db=db,
vendor_id=vendor_id,
addon_code=request.addon_code,
domain_name=request.domain_name,
quantity=request.quantity,
success_url=success_url,
cancel_url=cancel_url,
)
db.commit()
return result
@vendor_addons_router.delete("/{addon_id}", response_model=AddOnCancelResponse)
def cancel_addon(
addon_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Cancel a purchased add-on."""
vendor_id = current_user.token_vendor_id
result = billing_service.cancel_addon(db, vendor_id, addon_id)
db.commit()
return AddOnCancelResponse(
message=result["message"],
addon_code=result["addon_code"],
)