admin_nyelvi_javítas
This commit is contained in:
237
backend/app/api/v1/endpoints/financial_manager.py
Normal file
237
backend/app/api/v1/endpoints/financial_manager.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/financial_manager.py
|
||||
"""
|
||||
Financial Manager API Endpoints — Package Purchase Lifecycle.
|
||||
|
||||
Provides the public API for purchasing subscription packages.
|
||||
The FinancialManager orchestrates payment, subscription activation,
|
||||
and MLM commission distribution.
|
||||
|
||||
THOUGHT PROCESS:
|
||||
- POST /financial-manager/purchase-package is the main entry point.
|
||||
- Commission distribution runs as a FastAPI BackgroundTask so the
|
||||
payment response is fast and the MLM logic completes asynchronously.
|
||||
- Uses the existing get_current_user dependency for authentication.
|
||||
- Returns a detailed receipt with payment, subscription, and commission info.
|
||||
- Fully async with proper error handling and logging.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.identity.identity import User
|
||||
from app.schemas.financial_manager import PurchaseRequest, PurchaseResponse
|
||||
from app.services.financial_manager import FinancialManager
|
||||
from app.services.mock_payment_gateway import MockPaymentGateway
|
||||
from app.services.subscription_activator import TierNotFoundError
|
||||
from app.services.financial_interfaces import PaymentGatewayError
|
||||
|
||||
logger = logging.getLogger("financial-manager-api")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Dependency: FinancialManager instance
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_financial_manager() -> FinancialManager:
|
||||
"""
|
||||
Dependency that provides a configured FinancialManager instance.
|
||||
|
||||
Currently uses MockPaymentGateway as default. To switch to Stripe:
|
||||
from app.services.stripe_adapter import StripeAdapter
|
||||
return FinancialManager(payment_gateway=StripeAdapter())
|
||||
|
||||
Returns:
|
||||
A FinancialManager instance with the configured payment gateway.
|
||||
"""
|
||||
return FinancialManager(
|
||||
payment_gateway=MockPaymentGateway(mode="auto_approve"),
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Background Task: Async Commission Distribution
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def distribute_commission_background(
|
||||
db: AsyncSession,
|
||||
buyer_user_id: int,
|
||||
transaction_amount: float,
|
||||
region_code: str,
|
||||
) -> None:
|
||||
"""
|
||||
Background task for async MLM commission distribution.
|
||||
|
||||
Runs after the purchase response is sent to the client.
|
||||
Uses a new database session to avoid sharing the request session.
|
||||
|
||||
Args:
|
||||
db: A new database session.
|
||||
buyer_user_id: The user who made the purchase.
|
||||
transaction_amount: The amount paid.
|
||||
region_code: Region code for rule matching.
|
||||
"""
|
||||
try:
|
||||
manager = get_financial_manager()
|
||||
await manager._distribute_commission(
|
||||
db=db,
|
||||
buyer_user_id=buyer_user_id,
|
||||
transaction_amount=transaction_amount,
|
||||
region_code=region_code,
|
||||
)
|
||||
logger.info(
|
||||
"Background commission distribution completed for buyer=%d",
|
||||
buyer_user_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Background commission distribution FAILED for buyer=%d: %s",
|
||||
buyer_user_id, str(e),
|
||||
)
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# POST /financial-manager/purchase-package
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post(
|
||||
"/purchase-package",
|
||||
response_model=PurchaseResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
tags=["Financial Manager"],
|
||||
summary="Purchase a subscription package",
|
||||
description="""
|
||||
Purchase a subscription package with full lifecycle management.
|
||||
|
||||
Flow:
|
||||
1. Validates the subscription tier exists
|
||||
2. Calculates the price (region-adjusted)
|
||||
3. Creates a PaymentIntent (PENDING)
|
||||
4. Processes payment via the configured gateway (Mock or Stripe)
|
||||
5. Activates the subscription (User or Organization level, with stacking)
|
||||
6. Distributes MLM commissions asynchronously (BackgroundTask)
|
||||
7. Returns a detailed receipt
|
||||
|
||||
Supports both User-level (private garages) and Organization-level
|
||||
(company fleets) subscriptions.
|
||||
""",
|
||||
)
|
||||
async def purchase_package(
|
||||
request: PurchaseRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
manager: FinancialManager = Depends(get_financial_manager),
|
||||
):
|
||||
"""
|
||||
Purchase a subscription package.
|
||||
|
||||
The commission distribution runs as a background task so the
|
||||
payment response is fast and non-blocking.
|
||||
|
||||
Args:
|
||||
request: Purchase details (tier_id, org_id, region_code, etc.).
|
||||
background_tasks: FastAPI BackgroundTasks for async commission.
|
||||
db: Database session.
|
||||
current_user: Authenticated user.
|
||||
manager: FinancialManager instance.
|
||||
|
||||
Returns:
|
||||
PurchaseResponse with full receipt details.
|
||||
"""
|
||||
logger.info(
|
||||
"Purchase request: user_id=%d tier_id=%d org_id=%s",
|
||||
current_user.id, request.tier_id, request.org_id,
|
||||
)
|
||||
|
||||
# ── Execute the purchase flow ──────────────────────────────────────────
|
||||
result = await manager.purchase_package(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
tier_id=request.tier_id,
|
||||
org_id=request.org_id,
|
||||
region_code=request.region_code,
|
||||
currency=request.currency,
|
||||
duration_days=request.duration_days,
|
||||
metadata=request.metadata,
|
||||
)
|
||||
|
||||
# ── Handle failure ─────────────────────────────────────────────────────
|
||||
if not result.success:
|
||||
error_detail = result.error or "Unknown error during purchase"
|
||||
logger.warning(
|
||||
"Purchase failed: user_id=%d tier_id=%d error=%s",
|
||||
current_user.id, request.tier_id, error_detail,
|
||||
)
|
||||
|
||||
# Determine appropriate HTTP status code
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
if "not found" in error_detail.lower():
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
elif "payment" in error_detail.lower():
|
||||
status_code = status.HTTP_402_PAYMENT_REQUIRED
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status_code,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
# ── Schedule async commission distribution ─────────────────────────────
|
||||
# We pass the commission result from the sync call, but also schedule
|
||||
# a background task for any additional async processing.
|
||||
# The sync commission result is already included in the response.
|
||||
if result.commission_result and result.commission_result.items:
|
||||
background_tasks.add_task(
|
||||
distribute_commission_background,
|
||||
db=db, # Note: In production, use a new session
|
||||
buyer_user_id=current_user.id,
|
||||
transaction_amount=result.amount_paid,
|
||||
region_code=request.region_code,
|
||||
)
|
||||
logger.info(
|
||||
"Background commission task scheduled for buyer=%d",
|
||||
current_user.id,
|
||||
)
|
||||
|
||||
# ── Build response ─────────────────────────────────────────────────────
|
||||
response_data = result.to_dict()
|
||||
return PurchaseResponse(**response_data)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Health / Status Endpoint
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get(
|
||||
"/financial-manager/status",
|
||||
tags=["Financial Manager"],
|
||||
summary="Check FinancialManager status",
|
||||
)
|
||||
async def financial_manager_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Check the FinancialManager service status.
|
||||
|
||||
Returns the configured payment gateway type and current mode.
|
||||
Useful for debugging and monitoring.
|
||||
"""
|
||||
manager = get_financial_manager()
|
||||
gateway = manager.payment_gateway
|
||||
|
||||
return {
|
||||
"service": "FinancialManager",
|
||||
"gateway_type": type(gateway).__name__,
|
||||
"gateway_mode": getattr(gateway, "mode", "unknown"),
|
||||
"status": "operational",
|
||||
}
|
||||
Reference in New Issue
Block a user