274 lines
12 KiB
Python
274 lines
12 KiB
Python
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/admin_commission.py
|
|
"""
|
|
Admin API endpoints for CommissionRule CRUD, Priority Resolution Engine,
|
|
and 2-Level MLM Commission Distribution.
|
|
|
|
THOUGHT PROCESS:
|
|
- All endpoints are prefixed with /admin/commission-rules (registered in api.py).
|
|
- Staff-level access via get_current_staff dependency (SUPERADMIN, ADMIN, etc.).
|
|
- The GET /active endpoint implements the Priority Resolution Algorithm
|
|
(Campaign > Region > Tier) from the service layer.
|
|
- Pagination, filtering by type/tier/region/active/campaign are supported.
|
|
- Soft-delete via PATCH setting is_active=False; hard DELETE also available.
|
|
- POST /distribute implements the 2-Level MLM commission distribution:
|
|
Gen1 (direct referrer) gets commission_percent, Gen2 (upline) gets
|
|
upline_commission_percent from the same rule.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import date
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_current_staff
|
|
from app.db.session import get_db
|
|
from app.models.identity.identity import User
|
|
from app.models.marketplace.commission import (
|
|
CommissionRule,
|
|
CommissionRuleType,
|
|
CommissionTier,
|
|
)
|
|
from app.schemas.commission import (
|
|
CommissionRuleCreate,
|
|
CommissionRuleUpdate,
|
|
CommissionRuleResponse,
|
|
CommissionRuleListResponse,
|
|
CommissionDistributionRequest,
|
|
CommissionDistributionResponse,
|
|
)
|
|
from app.services import commission_service
|
|
|
|
logger = logging.getLogger("admin-commission")
|
|
router = APIRouter()
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# LIST: GET /admin/commission-rules
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get(
|
|
"",
|
|
response_model=CommissionRuleListResponse,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def list_commission_rules(
|
|
page: int = Query(1, ge=1, description="Oldalszám"),
|
|
page_size: int = Query(20, ge=1, le=100, description="Elemek száma oldalanként"),
|
|
rule_type: Optional[CommissionRuleType] = Query(None, description="Szűrés típus szerint (L1_REWARD / L2_COMMISSION)"),
|
|
tier: Optional[CommissionTier] = Query(None, description="Szűrés szint szerint (STANDARD / VIP / PLATINUM / ENTERPRISE)"),
|
|
region_code: Optional[str] = Query(None, max_length=10, description="Szűrés régió szerint (pl. HU, GLOBAL)"),
|
|
is_active: Optional[bool] = Query(None, description="Szűrés aktív/inaktív státusz szerint"),
|
|
is_campaign: Optional[bool] = Query(None, description="Szűrés kampány/állandó szabály szerint"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""List all commission rules with optional filters and pagination."""
|
|
rules, total = await commission_service.list_rules(
|
|
db,
|
|
page=page,
|
|
page_size=page_size,
|
|
rule_type=rule_type,
|
|
tier=tier,
|
|
region_code=region_code,
|
|
is_active=is_active,
|
|
is_campaign=is_campaign,
|
|
)
|
|
return CommissionRuleListResponse(
|
|
items=[CommissionRuleResponse.model_validate(r) for r in rules],
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# GET ACTIVE RULE: GET /admin/commission-rules/active
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get(
|
|
"/active",
|
|
response_model=CommissionRuleResponse,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def get_active_commission_rule(
|
|
rule_type: CommissionRuleType = Query(..., description="Jutalék típusa (L1_REWARD / L2_COMMISSION)"),
|
|
tier: CommissionTier = Query(CommissionTier.STANDARD, description="Felhasználó szintje"),
|
|
region_code: str = Query("GLOBAL", max_length=10, description="Régiókód (ISO 3166-1 alpha-2)"),
|
|
transaction_date: date = Query(..., description="Tranzakció dátuma (ISO 8601, pl. 2026-07-24)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""
|
|
Priority Resolution Engine: Find the most specific active rule.
|
|
|
|
Resolution order:
|
|
1. Campaign rules over permanent rules
|
|
2. Most specific region match (HU > GLOBAL)
|
|
3. Highest tier match (PLATINUM > VIP > STANDARD > ENTERPRISE)
|
|
"""
|
|
rule = await commission_service.get_active_rule(
|
|
db,
|
|
rule_type=rule_type,
|
|
tier=tier,
|
|
region_code=region_code,
|
|
transaction_date=transaction_date,
|
|
)
|
|
if not rule:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="No active commission rule found for the given parameters.",
|
|
)
|
|
return CommissionRuleResponse.model_validate(rule)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# GET SINGLE: GET /admin/commission-rules/{rule_id}
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get(
|
|
"/{rule_id}",
|
|
response_model=CommissionRuleResponse,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def get_commission_rule(
|
|
rule_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""Get a single commission rule by ID."""
|
|
from sqlalchemy import select
|
|
stmt = select(CommissionRule).where(CommissionRule.id == rule_id)
|
|
result = await db.execute(stmt)
|
|
rule = result.scalar_one_or_none()
|
|
if not rule:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Commission rule with id={rule_id} not found.",
|
|
)
|
|
return CommissionRuleResponse.model_validate(rule)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# CREATE: POST /admin/commission-rules
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post(
|
|
"",
|
|
response_model=CommissionRuleResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def create_commission_rule(
|
|
data: CommissionRuleCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""Create a new commission rule."""
|
|
rule = await commission_service.create_commission_rule(
|
|
db, data=data, admin_user_id=current_user.id,
|
|
)
|
|
return CommissionRuleResponse.model_validate(rule)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# UPDATE: PUT /admin/commission-rules/{rule_id}
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.put(
|
|
"/{rule_id}",
|
|
response_model=CommissionRuleResponse,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def update_commission_rule(
|
|
rule_id: int,
|
|
data: CommissionRuleUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""Update an existing commission rule (partial update)."""
|
|
rule = await commission_service.update_commission_rule(db, rule_id, data)
|
|
if not rule:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Commission rule with id={rule_id} not found.",
|
|
)
|
|
return CommissionRuleResponse.model_validate(rule)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# SOFT DELETE (DEACTIVATE): DELETE /admin/commission-rules/{rule_id}
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.delete(
|
|
"/{rule_id}",
|
|
status_code=status.HTTP_200_OK,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def deactivate_commission_rule(
|
|
rule_id: int,
|
|
hard_delete: bool = Query(False, description="Végleges törlés (alapértelmezett: soft-delete / deaktiválás)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""
|
|
Deactivate (soft-delete) or permanently delete a commission rule.
|
|
|
|
By default, sets is_active=False (soft delete).
|
|
Use ?hard_delete=true for permanent removal.
|
|
"""
|
|
if hard_delete:
|
|
deleted = await commission_service.hard_delete_commission_rule(db, rule_id)
|
|
if not deleted:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Commission rule with id={rule_id} not found.",
|
|
)
|
|
return {"message": f"Commission rule {rule_id} permanently deleted."}
|
|
|
|
rule = await commission_service.deactivate_commission_rule(db, rule_id)
|
|
if not rule:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Commission rule with id={rule_id} not found.",
|
|
)
|
|
return CommissionRuleResponse.model_validate(rule)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# 2-LEVEL MLM DISTRIBUTION: POST /admin/commission-rules/distribute
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post(
|
|
"/distribute",
|
|
response_model=CommissionDistributionResponse,
|
|
tags=["Admin Commission Rules"],
|
|
)
|
|
async def distribute_commission(
|
|
request: CommissionDistributionRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_staff),
|
|
):
|
|
"""
|
|
2-Level MLM Commission Distribution Engine.
|
|
|
|
When a referred company makes a purchase, this endpoint:
|
|
1. Looks up the buyer's direct referrer (Gen1)
|
|
2. Finds the active L2_COMMISSION rule for Gen1's tier/region
|
|
3. Calculates Gen1's commission using commission_percent
|
|
4. If Gen1 has a referrer (Gen2/upline), calculates Gen2's commission
|
|
using upline_commission_percent from the same rule
|
|
|
|
Returns a breakdown of all commission payouts.
|
|
"""
|
|
result = await commission_service.distribute_commission(db, request)
|
|
return result
|