L1L2 gen1Gen2 kialakítása

This commit is contained in:
Roo
2026-07-24 09:56:21 +00:00
parent 4594de6c11
commit 33c4d793db
49 changed files with 4108 additions and 127 deletions

View File

@@ -0,0 +1,273 @@
# /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