Files
orion/app/modules/billing/routes/api/store_addons.py
Samir Boulahtit 0d1007282a
Some checks failed
CI / ruff (push) Successful in 15s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
feat(config): add APP_BASE_URL setting for outbound link construction
Adds app_base_url config (default http://localhost:8000) used for all
outbound URLs: invitation emails, billing checkout redirects, signup
login links, portal return URLs.

Replaces hardcoded https://{main_domain} and localhost:8000 patterns.
Configurable per environment via APP_BASE_URL env var:
- Dev: http://localhost:8000 (or http://acme.localhost:9999)
- Prod: https://wizard.lu

main_domain is preserved for subdomain resolution and cookie config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:43:36 +02:00

181 lines
5.0 KiB
Python

# app/modules/billing/routes/api/store_addons.py
"""
Store add-on management endpoints.
Provides:
- List available add-ons
- Get store'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_store_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 app.modules.enums import FrontendType
from app.modules.tenancy.schemas.auth import UserContext
store_addons_router = APIRouter(
prefix="/addons",
dependencies=[Depends(require_module_access("billing", FrontendType.STORE))],
)
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 StoreAddOnResponse(BaseModel):
"""Store'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
# ============================================================================
@store_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_store_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
]
@store_addons_router.get("/my-addons", response_model=list[StoreAddOnResponse])
def get_store_addons(
current_user: UserContext = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""Get store's purchased add-ons."""
store_id = current_user.token_store_id
store_addons = billing_service.get_store_addons(db, store_id)
return [
StoreAddOnResponse(
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 store_addons
]
@store_addons_router.post("/purchase")
def purchase_addon(
request: AddOnPurchaseRequest,
current_user: UserContext = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""Purchase an add-on product."""
store_id = current_user.token_store_id
store = billing_service.get_store(db, store_id)
# Build URLs
base_url = settings.app_base_url.rstrip("/")
success_url = f"{base_url}/store/{store.store_code}/billing?addon_success=true"
cancel_url = f"{base_url}/store/{store.store_code}/billing?addon_cancelled=true"
result = billing_service.purchase_addon(
db=db,
store_id=store_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
@store_addons_router.delete("/{addon_id}", response_model=AddOnCancelResponse)
def cancel_addon(
addon_id: int,
current_user: UserContext = Depends(get_current_store_api),
db: Session = Depends(get_db),
):
"""Cancel a purchased add-on."""
store_id = current_user.token_store_id
result = billing_service.cancel_addon(db, store_id, addon_id)
db.commit()
return AddOnCancelResponse(
message=result["message"],
addon_code=result["addon_code"],
)