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>
This commit is contained in:
2026-01-31 13:49:58 +01:00
parent 6f278131a3
commit e2cecff014
13 changed files with 495 additions and 614 deletions

View File

@@ -213,3 +213,17 @@ def get_invoices(
# NOTE: Additional endpoints (checkout, portal, cancel, addons, etc.)
# are still handled by app/api/v1/vendor/billing.py for now.
# They can be migrated here as part of a larger refactoring effort.
# ============================================================================
# Aggregate Sub-Routers
# ============================================================================
# Include all billing-related vendor sub-routers
from app.modules.billing.routes.api.vendor_features import vendor_features_router
from app.modules.billing.routes.api.vendor_checkout import vendor_checkout_router
from app.modules.billing.routes.api.vendor_addons import vendor_addons_router
vendor_router.include_router(vendor_features_router, tags=["vendor-features"])
vendor_router.include_router(vendor_checkout_router, tags=["vendor-billing"])
vendor_router.include_router(vendor_addons_router, tags=["vendor-billing-addons"])

View File

@@ -0,0 +1,179 @@
# 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"],
)

View File

@@ -0,0 +1,220 @@
# app/modules/billing/routes/api/vendor_checkout.py
"""
Vendor checkout and subscription management endpoints.
Provides:
- Stripe checkout session creation
- Stripe portal session creation
- Subscription cancellation and reactivation
- Upcoming invoice preview
- Tier changes (upgrade/downgrade)
All routes require module access control for the 'billing' module.
"""
import logging
from fastapi import APIRouter, Depends
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, subscription_service
from models.schema.auth import UserContext
vendor_checkout_router = APIRouter(
dependencies=[Depends(require_module_access("billing"))],
)
logger = logging.getLogger(__name__)
# ============================================================================
# Schemas
# ============================================================================
class CheckoutRequest(BaseModel):
"""Request to create a checkout session."""
tier_code: str
is_annual: bool = False
class CheckoutResponse(BaseModel):
"""Checkout session response."""
checkout_url: str
session_id: str
class PortalResponse(BaseModel):
"""Customer portal session response."""
portal_url: str
class CancelRequest(BaseModel):
"""Request to cancel subscription."""
reason: str | None = None
immediately: bool = False
class CancelResponse(BaseModel):
"""Cancellation response."""
message: str
effective_date: str
class UpcomingInvoiceResponse(BaseModel):
"""Upcoming invoice preview."""
amount_due_cents: int
currency: str
next_payment_date: str | None = None
line_items: list[dict] = []
class ChangeTierRequest(BaseModel):
"""Request to change subscription tier."""
tier_code: str
is_annual: bool = False
class ChangeTierResponse(BaseModel):
"""Response for tier change."""
message: str
new_tier: str
effective_immediately: bool
# ============================================================================
# Endpoints
# ============================================================================
@vendor_checkout_router.post("/checkout", response_model=CheckoutResponse)
def create_checkout_session(
request: CheckoutRequest,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Create a Stripe checkout session for subscription."""
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?success=true"
cancel_url = f"{base_url}/vendor/{vendor.vendor_code}/billing?cancelled=true"
result = billing_service.create_checkout_session(
db=db,
vendor_id=vendor_id,
tier_code=request.tier_code,
is_annual=request.is_annual,
success_url=success_url,
cancel_url=cancel_url,
)
db.commit()
return CheckoutResponse(checkout_url=result["checkout_url"], session_id=result["session_id"])
@vendor_checkout_router.post("/portal", response_model=PortalResponse)
def create_portal_session(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Create a Stripe customer portal session."""
vendor_id = current_user.token_vendor_id
vendor = billing_service.get_vendor(db, vendor_id)
return_url = f"https://{settings.platform_domain}/vendor/{vendor.vendor_code}/billing"
result = billing_service.create_portal_session(db, vendor_id, return_url)
return PortalResponse(portal_url=result["portal_url"])
@vendor_checkout_router.post("/cancel", response_model=CancelResponse)
def cancel_subscription(
request: CancelRequest,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Cancel subscription."""
vendor_id = current_user.token_vendor_id
result = billing_service.cancel_subscription(
db=db,
vendor_id=vendor_id,
reason=request.reason,
immediately=request.immediately,
)
db.commit()
return CancelResponse(
message=result["message"],
effective_date=result["effective_date"],
)
@vendor_checkout_router.post("/reactivate")
def reactivate_subscription(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Reactivate a cancelled subscription."""
vendor_id = current_user.token_vendor_id
result = billing_service.reactivate_subscription(db, vendor_id)
db.commit()
return result
@vendor_checkout_router.get("/upcoming-invoice", response_model=UpcomingInvoiceResponse)
def get_upcoming_invoice(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Preview the upcoming invoice."""
vendor_id = current_user.token_vendor_id
result = billing_service.get_upcoming_invoice(db, vendor_id)
return UpcomingInvoiceResponse(
amount_due_cents=result.get("amount_due_cents", 0),
currency=result.get("currency", "EUR"),
next_payment_date=result.get("next_payment_date"),
line_items=result.get("line_items", []),
)
@vendor_checkout_router.post("/change-tier", response_model=ChangeTierResponse)
def change_tier(
request: ChangeTierRequest,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""Change subscription tier (upgrade/downgrade)."""
vendor_id = current_user.token_vendor_id
result = billing_service.change_tier(
db=db,
vendor_id=vendor_id,
new_tier_code=request.tier_code,
is_annual=request.is_annual,
)
db.commit()
return ChangeTierResponse(
message=result["message"],
new_tier=result["new_tier"],
effective_immediately=result["effective_immediately"],
)

View File

@@ -282,9 +282,11 @@ def ship_order_item(
# Aggregate routers
# ============================================================================
# Import exceptions router
# Import sub-routers
from app.modules.orders.routes.api.vendor_exceptions import vendor_exceptions_router
from app.modules.orders.routes.api.vendor_invoices import vendor_invoices_router
# Include both routers into the aggregate vendor_router
# Include all sub-routers into the aggregate vendor_router
vendor_router.include_router(_orders_router, tags=["vendor-orders"])
vendor_router.include_router(vendor_exceptions_router, tags=["vendor-order-exceptions"])
vendor_router.include_router(vendor_invoices_router, tags=["vendor-invoices"])

View File

@@ -0,0 +1,348 @@
# app/modules/orders/routes/api/vendor_invoices.py
"""
Vendor invoice management endpoints.
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The get_current_vendor_api dependency guarantees token_vendor_id is present.
Endpoints:
- GET /invoices - List vendor invoices
- GET /invoices/{invoice_id} - Get invoice details
- POST /invoices - Create invoice from order
- PUT /invoices/{invoice_id}/status - Update invoice status
- GET /invoices/{invoice_id}/pdf - Download invoice PDF
- POST /invoices/{invoice_id}/pdf - Generate/regenerate invoice PDF
- GET /invoices/settings - Get invoice settings
- POST /invoices/settings - Create invoice settings
- PUT /invoices/settings - Update invoice settings
- GET /invoices/stats - Get invoice statistics
Feature Requirements:
- invoice_lu: Basic Luxembourg invoicing (Essential tier)
- invoice_eu_vat: EU VAT support (Professional tier)
- invoice_bulk: Bulk invoicing (Business tier)
- accounting_export: Export to accounting software (Business tier)
"""
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, Query
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.core.feature_gate import RequireFeature
from app.exceptions.invoice import (
InvoicePDFNotFoundException,
)
from app.services.invoice_service import invoice_service
from app.modules.billing.models import FeatureCode
from models.schema.auth import UserContext
from app.modules.orders.schemas import (
InvoiceCreate,
InvoiceListPaginatedResponse,
InvoiceListResponse,
InvoicePDFGeneratedResponse,
InvoiceResponse,
InvoiceStatsResponse,
InvoiceStatusUpdate,
VendorInvoiceSettingsCreate,
VendorInvoiceSettingsResponse,
VendorInvoiceSettingsUpdate,
)
vendor_invoices_router = APIRouter(
prefix="/invoices",
dependencies=[Depends(require_module_access("orders"))],
)
logger = logging.getLogger(__name__)
# ============================================================================
# Invoice Settings
# ============================================================================
@vendor_invoices_router.get("/settings", response_model=VendorInvoiceSettingsResponse | None)
def get_invoice_settings(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
_: None = Depends(RequireFeature(FeatureCode.INVOICE_LU)),
):
"""
Get vendor invoice settings.
Returns null if settings not yet configured.
Requires: invoice_lu feature (Essential tier)
"""
settings = invoice_service.get_settings(db, current_user.token_vendor_id)
if settings:
return VendorInvoiceSettingsResponse.model_validate(settings)
return None
@vendor_invoices_router.post("/settings", response_model=VendorInvoiceSettingsResponse, status_code=201)
def create_invoice_settings(
data: VendorInvoiceSettingsCreate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Create vendor invoice settings.
Required before creating invoices. Sets company details,
VAT number, invoice numbering preferences, and payment info.
"""
settings = invoice_service.create_settings(
db=db,
vendor_id=current_user.token_vendor_id,
data=data,
)
return VendorInvoiceSettingsResponse.model_validate(settings)
@vendor_invoices_router.put("/settings", response_model=VendorInvoiceSettingsResponse)
def update_invoice_settings(
data: VendorInvoiceSettingsUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update vendor invoice settings.
"""
settings = invoice_service.update_settings(
db=db,
vendor_id=current_user.token_vendor_id,
data=data,
)
return VendorInvoiceSettingsResponse.model_validate(settings)
# ============================================================================
# Invoice Statistics
# ============================================================================
@vendor_invoices_router.get("/stats", response_model=InvoiceStatsResponse)
def get_invoice_stats(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get invoice statistics for the vendor.
Returns:
- total_invoices: Total number of invoices
- total_revenue: Total revenue from issued/paid invoices (EUR)
- draft_count: Number of draft invoices
- paid_count: Number of paid invoices
"""
stats = invoice_service.get_invoice_stats(db, current_user.token_vendor_id)
return InvoiceStatsResponse(
total_invoices=stats.get("total_invoices", 0),
total_revenue_cents=stats.get("total_revenue_cents", 0),
draft_count=stats.get("draft_count", 0),
issued_count=stats.get("issued_count", 0),
paid_count=stats.get("paid_count", 0),
cancelled_count=stats.get("cancelled_count", 0),
)
# ============================================================================
# Invoice CRUD
# ============================================================================
@vendor_invoices_router.get("", response_model=InvoiceListPaginatedResponse)
def list_invoices(
page: int = Query(1, ge=1, description="Page number"),
per_page: int = Query(20, ge=1, le=100, description="Items per page"),
status: str | None = Query(None, description="Filter by status"),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
List vendor invoices with pagination.
Supports filtering by status: draft, issued, paid, cancelled
"""
invoices, total = invoice_service.list_invoices(
db=db,
vendor_id=current_user.token_vendor_id,
status=status,
page=page,
per_page=per_page,
)
# Build response with buyer name from snapshot
items = []
for inv in invoices:
buyer_name = inv.buyer_details.get("name") if inv.buyer_details else None
items.append(
InvoiceListResponse(
id=inv.id,
invoice_number=inv.invoice_number,
invoice_date=inv.invoice_date,
status=inv.status,
currency=inv.currency,
total_cents=inv.total_cents,
order_id=inv.order_id,
buyer_name=buyer_name,
)
)
pages = (total + per_page - 1) // per_page if total > 0 else 1
return InvoiceListPaginatedResponse(
items=items,
total=total,
page=page,
per_page=per_page,
pages=pages,
)
@vendor_invoices_router.get("/{invoice_id}", response_model=InvoiceResponse)
def get_invoice(
invoice_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get invoice details by ID.
"""
invoice = invoice_service.get_invoice_or_raise(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
)
return InvoiceResponse.model_validate(invoice)
@vendor_invoices_router.post("", response_model=InvoiceResponse, status_code=201)
def create_invoice(
data: InvoiceCreate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Create an invoice from an order.
Automatically:
- Captures seller/buyer details snapshots
- Calculates VAT based on buyer country and OSS settings
- Generates invoice number
"""
invoice = invoice_service.create_invoice_from_order(
db=db,
vendor_id=current_user.token_vendor_id,
order_id=data.order_id,
notes=data.notes,
)
return InvoiceResponse.model_validate(invoice)
@vendor_invoices_router.put("/{invoice_id}/status", response_model=InvoiceResponse)
def update_invoice_status(
invoice_id: int,
data: InvoiceStatusUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update invoice status.
Valid statuses:
- draft: Invoice created but not yet sent
- issued: Invoice sent to customer
- paid: Payment received
- cancelled: Invoice cancelled
Note: Cancelled invoices cannot be modified.
"""
invoice = invoice_service.update_status(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
new_status=data.status,
)
return InvoiceResponse.model_validate(invoice)
# ============================================================================
# PDF Generation
# ============================================================================
@vendor_invoices_router.post("/{invoice_id}/pdf", response_model=InvoicePDFGeneratedResponse)
def generate_invoice_pdf(
invoice_id: int,
regenerate: bool = Query(False, description="Force regenerate if exists"),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Generate PDF for an invoice.
Set regenerate=true to force regeneration if PDF already exists.
Returns the path to the generated PDF.
"""
pdf_path = invoice_service.generate_pdf(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
force_regenerate=regenerate,
)
return InvoicePDFGeneratedResponse(pdf_path=pdf_path)
@vendor_invoices_router.get("/{invoice_id}/pdf")
def download_invoice_pdf(
invoice_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Download invoice PDF.
Returns the PDF file for download.
Generates PDF if not already generated.
"""
# Check if PDF exists, generate if not
pdf_path = invoice_service.get_pdf_path(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
)
if not pdf_path:
# Generate PDF
pdf_path = invoice_service.generate_pdf(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
)
# Verify file exists
if not Path(pdf_path).exists():
raise InvoicePDFNotFoundException(invoice_id)
# Get invoice for filename
invoice = invoice_service.get_invoice_or_raise(
db=db,
vendor_id=current_user.token_vendor_id,
invoice_id=invoice_id,
)
filename = f"invoice-{invoice.invoice_number}.pdf"
return FileResponse(
path=pdf_path,
media_type="application/pdf",
filename=filename,
headers={
"Content-Disposition": f'attachment; filename="{filename}"'
},
)

View File

@@ -21,14 +21,14 @@ from models.database.admin_menu_config import FrontendType
def _get_admin_router():
"""Lazy import of admin router to avoid circular imports."""
from app.modules.payments.routes.admin import admin_router
from app.modules.payments.routes.api.admin import admin_router
return admin_router
def _get_vendor_router():
"""Lazy import of vendor router to avoid circular imports."""
from app.modules.payments.routes.vendor import vendor_router
from app.modules.payments.routes.api.vendor import vendor_router
return vendor_router
@@ -61,6 +61,7 @@ payments_module = ModuleDefinition(
},
is_core=False,
is_internal=False,
is_self_contained=True, # Enable auto-discovery from routes/api/
)

View File

@@ -1,7 +1,11 @@
# app/modules/payments/routes/__init__.py
"""Payments module routes."""
"""
Payments module routes.
from app.modules.payments.routes.admin import admin_router
from app.modules.payments.routes.vendor import vendor_router
Re-exports routers from the api subdirectory for backwards compatibility.
"""
from app.modules.payments.routes.api.admin import admin_router
from app.modules.payments.routes.api.vendor import vendor_router
__all__ = ["admin_router", "vendor_router"]

View File

@@ -0,0 +1,13 @@
# app/modules/payments/routes/api/__init__.py
"""
Payments module API routes.
Provides REST API endpoints for payment management:
- Admin API: Payment gateway configuration, transaction monitoring, refunds
- Vendor API: Payment configuration, Stripe connect, transactions, balance
"""
from app.modules.payments.routes.api.admin import admin_router
from app.modules.payments.routes.api.vendor import vendor_router
__all__ = ["admin_router", "vendor_router"]

View File

@@ -1,4 +1,4 @@
# app/modules/payments/routes/admin.py
# app/modules/payments/routes/api/admin.py
"""
Admin routes for payments module.
@@ -8,9 +8,17 @@ Provides routes for:
- Refund management
"""
from fastapi import APIRouter
import logging
admin_router = APIRouter(prefix="/payments", tags=["Payments (Admin)"])
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
admin_router = APIRouter(
prefix="/payments",
dependencies=[Depends(require_module_access("payments"))],
)
logger = logging.getLogger(__name__)
@admin_router.get("/gateways")

View File

@@ -0,0 +1,210 @@
# app/modules/payments/routes/api/vendor.py
"""
Vendor payment configuration and processing endpoints.
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The get_current_vendor_api dependency guarantees token_vendor_id is present.
Provides:
- Payment gateway configuration
- Stripe connect/disconnect
- Payment methods listing
- Transaction history
- Payment balance
- Refund processing
"""
import logging
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.services.vendor_service import vendor_service
from models.schema.auth import UserContext
from app.modules.payments.schemas import (
PaymentBalanceResponse,
PaymentConfigResponse,
PaymentConfigUpdate,
PaymentConfigUpdateResponse,
PaymentMethodsResponse,
PaymentRefundRequest as RefundRequest,
PaymentRefundResponse as RefundResponse,
StripeConnectRequest,
StripeConnectResponse,
StripeDisconnectResponse,
TransactionsResponse,
)
vendor_router = APIRouter(
prefix="/payments",
dependencies=[Depends(require_module_access("payments"))],
)
logger = logging.getLogger(__name__)
@vendor_router.get("/config", response_model=PaymentConfigResponse)
def get_payment_configuration(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get vendor payment configuration.
TODO: Implement in Slice 5
- Get payment gateway settings (Stripe, PayPal, etc.)
- Get accepted payment methods
- Get currency settings
- Return masked/secure information only
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return PaymentConfigResponse(
payment_gateway=None,
accepted_methods=[],
currency="EUR",
stripe_connected=False,
message="Payment configuration coming in Slice 5",
)
@vendor_router.put("/config", response_model=PaymentConfigUpdateResponse)
def update_payment_configuration(
payment_config: PaymentConfigUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update vendor payment configuration.
TODO: Implement in Slice 5
- Update payment gateway settings
- Connect/disconnect Stripe account
- Update accepted payment methods
- Validate configuration before saving
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return PaymentConfigUpdateResponse(
message="Payment configuration update coming in Slice 5"
)
@vendor_router.post("/stripe/connect", response_model=StripeConnectResponse)
def connect_stripe_account(
stripe_data: StripeConnectRequest,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Connect Stripe account for payment processing.
TODO: Implement in Slice 5
- Handle Stripe OAuth flow
- Store Stripe account ID securely
- Verify Stripe account is active
- Enable payment processing
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return StripeConnectResponse(message="Stripe connection coming in Slice 5")
@vendor_router.delete("/stripe/disconnect", response_model=StripeDisconnectResponse)
def disconnect_stripe_account(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Disconnect Stripe account.
TODO: Implement in Slice 5
- Remove Stripe account connection
- Disable payment processing
- Warn about pending payments
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return StripeDisconnectResponse(message="Stripe disconnection coming in Slice 5")
@vendor_router.get("/methods", response_model=PaymentMethodsResponse)
def get_payment_methods(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get accepted payment methods for vendor.
TODO: Implement in Slice 5
- Return list of enabled payment methods
- Include: credit card, PayPal, bank transfer, etc.
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return PaymentMethodsResponse(
methods=[],
message="Payment methods coming in Slice 5",
)
@vendor_router.get("/transactions", response_model=TransactionsResponse)
def get_payment_transactions(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get payment transaction history.
TODO: Implement in Slice 5
- Get all payment transactions for vendor
- Filter by date range, status, etc.
- Include payment details
- Support pagination
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return TransactionsResponse(
transactions=[],
total=0,
message="Payment transactions coming in Slice 5",
)
@vendor_router.get("/balance", response_model=PaymentBalanceResponse)
def get_payment_balance(
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get vendor payment balance and payout information.
TODO: Implement in Slice 5
- Get available balance
- Get pending balance
- Get next payout date
- Get payout history
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return PaymentBalanceResponse(
available_balance=0.0,
pending_balance=0.0,
currency="EUR",
next_payout_date=None,
message="Payment balance coming in Slice 5",
)
@vendor_router.post("/refund/{payment_id}", response_model=RefundResponse)
def refund_payment(
payment_id: int,
refund_data: RefundRequest,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Process payment refund.
TODO: Implement in Slice 5
- Verify payment belongs to vendor
- Process refund through payment gateway
- Update order status
- Send refund notification to customer
"""
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841
return RefundResponse(message="Payment refund coming in Slice 5")

View File

@@ -1,40 +0,0 @@
# app/modules/payments/routes/vendor.py
"""
Vendor routes for payments module.
Provides routes for:
- Payment method management
- Transaction history
"""
from fastapi import APIRouter
vendor_router = APIRouter(prefix="/payments", tags=["Payments (Vendor)"])
@vendor_router.get("/methods")
async def list_payment_methods():
"""List saved payment methods for the vendor."""
# TODO: Implement payment method listing
return {"payment_methods": []}
@vendor_router.post("/methods")
async def add_payment_method():
"""Add a new payment method."""
# TODO: Implement payment method creation
return {"status": "created", "id": "pm_xxx"}
@vendor_router.delete("/methods/{method_id}")
async def remove_payment_method(method_id: str):
"""Remove a saved payment method."""
# TODO: Implement payment method deletion
return {"status": "deleted", "id": method_id}
@vendor_router.get("/transactions")
async def list_vendor_transactions():
"""List transactions for the vendor."""
# TODO: Implement transaction listing
return {"transactions": [], "total": 0}