L1L2 gen1Gen2 kialakítása
This commit is contained in:
273
backend/app/api/v1/endpoints/admin_commission.py
Normal file
273
backend/app/api/v1/endpoints/admin_commission.py
Normal 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
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.db.session import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.social_auth_service import SocialAuthService
|
||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||
from app.core.config import settings
|
||||
from app.services.config_service import config
|
||||
@@ -15,6 +16,10 @@ from app.models.identity import User, Device, UserDeviceLink # JAVÍTVA: Device-
|
||||
from app.core.translation_helper import t # Translation helper
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
from datetime import datetime, timezone
|
||||
import httpx
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -379,4 +384,153 @@ async def restore_account_verify(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=result.get("message", "Érvénytelen vagy lejárt kód.")
|
||||
)
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
# ── GOOGLE OAUTH ──
|
||||
|
||||
class GoogleAuthRequest(BaseModel):
|
||||
"""Google token from the frontend after successful Google Sign-In.
|
||||
|
||||
The frontend sends either an id_token (from Google Sign-In / One Tap)
|
||||
or an access_token (from Google OAuth2 token client).
|
||||
"""
|
||||
id_token: Optional[str] = Field(None, description="Google ID token (JWT) from Google Sign-In")
|
||||
access_token: Optional[str] = Field(None, description="Google OAuth2 access token from token client")
|
||||
referred_by_code: Optional[str] = Field(None, description="Optional referral code from registration form")
|
||||
|
||||
|
||||
@router.post("/google", response_model=Token)
|
||||
async def google_auth(
|
||||
request: GoogleAuthRequest,
|
||||
response: Response,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Google OAuth bejelentkezés / regisztráció.
|
||||
|
||||
A frontend Google Sign-In gombjára kattintva a felhasználó Google tokent kap.
|
||||
Ezt a tokent küldi el a backendnek, amely:
|
||||
1. Ellenőrzi a token érvényességét a Google nyilvános kulcsaival
|
||||
2. Kinyeri az email-t, nevet és Google ID-t
|
||||
3. Ha a felhasználó már létezik (Login): visszaadja a JWT tokent
|
||||
4. Ha nem létezik (Register): létrehozza az új felhasználót a Google adataival
|
||||
5. Opcionális referral_code tárolása, ha a regisztrációs űrlapon megadták
|
||||
|
||||
Supports both id_token (from Google Sign-In) and access_token (from OAuth2 token client).
|
||||
"""
|
||||
try:
|
||||
# 1. Determine which token was provided and build verification URL
|
||||
if request.id_token:
|
||||
google_verify_url = f"https://oauth2.googleapis.com/tokeninfo?id_token={request.id_token}"
|
||||
elif request.access_token:
|
||||
google_verify_url = f"https://oauth2.googleapis.com/tokeninfo?access_token={request.access_token}"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Hiányzó Google token. Adjon meg id_token vagy access_token értéket."
|
||||
)
|
||||
|
||||
# 2. Verify Google token
|
||||
async with httpx.AsyncClient() as client:
|
||||
google_resp = await client.get(google_verify_url)
|
||||
|
||||
if google_resp.status_code != 200:
|
||||
logger.error(f"Google token verification failed: {google_resp.text}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen Google token."
|
||||
)
|
||||
|
||||
google_data = google_resp.json()
|
||||
|
||||
# 3. Validate audience (client ID) — only for id_token
|
||||
# For access_token, the 'aud' field may not be present; validate by 'azp' or skip
|
||||
if request.id_token:
|
||||
if google_data.get("aud") != settings.GOOGLE_CLIENT_ID:
|
||||
logger.error(f"Google token audience mismatch: {google_data.get('aud')}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen Google token (audience mismatch)."
|
||||
)
|
||||
else:
|
||||
# For access_token, check azp (authorized party) matches our client ID
|
||||
if google_data.get("azp") != settings.GOOGLE_CLIENT_ID:
|
||||
logger.error(f"Google token azp mismatch: {google_data.get('azp')}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen Google token (azp mismatch)."
|
||||
)
|
||||
|
||||
# 4. Extract user info
|
||||
google_id = google_data.get("sub")
|
||||
email = google_data.get("email", "")
|
||||
first_name = google_data.get("given_name", "")
|
||||
last_name = google_data.get("family_name", "")
|
||||
|
||||
if not google_id or not email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Hiányzó Google felhasználói adatok."
|
||||
)
|
||||
|
||||
# 5. Get or create user via SocialAuthService
|
||||
user = await SocialAuthService.get_or_create_social_user(
|
||||
db=db,
|
||||
provider="google",
|
||||
social_id=google_id,
|
||||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
referred_by_code=request.referred_by_code
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Hiba a felhasználó létrehozása során."
|
||||
)
|
||||
|
||||
# 6. Generate JWT tokens (same logic as login)
|
||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
role_key = role_name.upper()
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"rank": ranks.get(role_key, 10),
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||
}
|
||||
|
||||
access, refresh = create_tokens(data=token_data, remember_me=False)
|
||||
|
||||
max_age_days = await config.get_setting(db, "auth_refresh_default_days", default=1)
|
||||
max_age_sec = int(max_age_days) * 24 * 60 * 60
|
||||
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=refresh,
|
||||
httponly=True,
|
||||
secure=True,
|
||||
samesite="lax",
|
||||
domain=settings.COOKIE_DOMAIN,
|
||||
max_age=max_age_sec
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"token_type": "bearer",
|
||||
"is_active": user.is_active
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Google auth error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Google hitelesítési hiba: {str(e)}"
|
||||
)
|
||||
@@ -87,7 +87,7 @@ async def list_body_types(
|
||||
current_user = Depends(deps.get_current_user)
|
||||
):
|
||||
"""Karosszéria-típus szótár lekérése. Opcionálisan szűrhető vehicle_class alapján."""
|
||||
stmt = select(BodyTypeDictionary).order_by(BodyTypeDictionary.name_hu)
|
||||
stmt = select(BodyTypeDictionary).order_by(BodyTypeDictionary.name_i18n)
|
||||
if vehicle_class:
|
||||
stmt = stmt.where(BodyTypeDictionary.vehicle_class == vehicle_class)
|
||||
result = await db.execute(stmt)
|
||||
@@ -97,10 +97,7 @@ async def list_body_types(
|
||||
"id": r.id,
|
||||
"vehicle_class": r.vehicle_class,
|
||||
"code": r.code,
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": r.name_i18n if r.name_i18n else {},
|
||||
# --- [DEPRECATED] Old columns (Phase 3) ---
|
||||
"name_hu": r.name_hu,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@@ -39,12 +39,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.fleet_finance.models import AssetCost, CostCategory
|
||||
from app.models.vehicle.asset import Asset
|
||||
from app.models.vehicle.asset_event import AssetEvent
|
||||
from app.models.vehicle.odometer import OdometerReading
|
||||
from app.models.fleet.organization import Organization
|
||||
from app.models.fleet.org_member import OrganizationMember
|
||||
from app.models.marketplace.service import ServiceProvider, ServiceProfile, ServiceExpertise
|
||||
from app.models.system.system_parameter import SystemParameter
|
||||
from app.models.vehicle.asset import AssetEvent
|
||||
from app.models.vehicle.asset import OdometerReading
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
from app.models.identity.social import ServiceProvider
|
||||
from app.models.marketplace.service import ServiceProfile, ServiceExpertise
|
||||
from app.models.system.system import SystemParameter
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostUpdate, AssetCostResponse
|
||||
from app.services.gamification_service import GamificationService
|
||||
from app.services.provider_service import find_or_create_provider_by_name
|
||||
|
||||
@@ -77,12 +77,8 @@ async def get_category_tree(
|
||||
nodes.append(CategoryTreeNode(
|
||||
id=tag.id,
|
||||
key=tag.key,
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
name_i18n=tag.name_i18n if tag.name_i18n else {},
|
||||
description_i18n=tag.description_i18n,
|
||||
# --- [DEPRECATED] Old columns (Phase 3) ---
|
||||
name_hu=tag.name_hu,
|
||||
name_en=tag.name_en,
|
||||
level=tag.level,
|
||||
path=tag.path,
|
||||
is_official=tag.is_official,
|
||||
@@ -111,21 +107,17 @@ async def autocomplete_categories(
|
||||
Szöveges kereső a Szint 2 (Szakma) és Szint 3 (Specifikus feladat) címkékhez.
|
||||
|
||||
Csak azokat listázza, ahol is_official=True.
|
||||
A keresés a name_i18n JSONB (minden nyelven), key és name_hu/name_en mezőkben történik.
|
||||
A keresés a name_i18n JSONB (minden nyelven) és key mezőkben történik.
|
||||
"""
|
||||
try:
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3): JSONB → String cast keresés ---
|
||||
stmt = select(ExpertiseTag).where(
|
||||
ExpertiseTag.level >= 2,
|
||||
ExpertiseTag.is_official == True,
|
||||
or_(
|
||||
cast(ExpertiseTag.name_i18n, String).ilike(f"%{q}%"),
|
||||
ExpertiseTag.key.ilike(f"%{q}%"),
|
||||
# --- [DEPRECATED] Old column fallback (Phase 3) ---
|
||||
ExpertiseTag.name_hu.ilike(f"%{q}%"),
|
||||
ExpertiseTag.name_en.ilike(f"%{q}%"),
|
||||
),
|
||||
).order_by(ExpertiseTag.level, ExpertiseTag.name_hu).limit(20)
|
||||
).order_by(ExpertiseTag.level, ExpertiseTag.name_i18n).limit(20)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
tags = result.scalars().all()
|
||||
@@ -134,11 +126,7 @@ async def autocomplete_categories(
|
||||
CategoryAutocompleteItem(
|
||||
id=t.id,
|
||||
key=t.key,
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
name_i18n=t.name_i18n if t.name_i18n else {},
|
||||
# --- [DEPRECATED] Old columns (Phase 3) ---
|
||||
name_hu=t.name_hu,
|
||||
name_en=t.name_en,
|
||||
level=t.level,
|
||||
path=t.path,
|
||||
parent_id=t.parent_id,
|
||||
|
||||
Reference in New Issue
Block a user