garázs fejlesztés

This commit is contained in:
Roo
2026-07-20 11:17:52 +00:00
parent 2e0abc62a7
commit 4594de6c11
80 changed files with 9135 additions and 8702 deletions

View File

@@ -2,10 +2,11 @@
👤 Admin Users API
Végpontok:
GET /admin/users/stats — Felhasználói statisztikák (dashboard)
GET /admin/users/{user_id} — Felhasználó részletes adatai (User + Person + Address)
PATCH /admin/users/{user_id} — Felhasználó szerkesztése admin által
GET /admin/users/{user_id}/memberships — Felhasználó szervezeti tagságai
GET /admin/users/stats — Felhasználói statisztikák (dashboard)
GET /admin/users/{user_id} — Felhasználó részletes adatai (User + Person + Address)
PATCH /admin/users/{user_id} — Felhasználó szerkesztése admin által
GET /admin/users/{user_id}/memberships — Felhasználó szervezeti tagságai
DELETE /admin/users/{user_id}/hard — [SUPERADMIN] Végleges felhasználó törlés
Megjegyzés: A GET /admin/users (lista) és POST /admin/users/bulk-action
végpontok a admin.py modulban találhatók, mivel az a /admin prefix alá
@@ -28,7 +29,9 @@ from app.api import deps
from app.db.session import get_db
from app.models.identity import User, Person, UserRole
from app.models.identity.address import Address
from app.models.identity.identity import Wallet
from app.models.marketplace.organization import Organization, OrganizationMember
from app.models.system.audit import SecurityAuditLog, FinancialLedger
from app.models.vehicle.history import AuditLog, LogSeverity
from app.schemas.user import UserResponse
from app.schemas.organization import OrganizationMemberResponse
@@ -644,3 +647,124 @@ async def get_user_memberships(
))
return membership_list
# =============================================================================
# Hard Delete Endpoint (Danger Zone)
# =============================================================================
@router.delete(
"/{user_id}/hard",
summary="[SUPERADMIN] Felhasználó végleges törlése",
description=(
"Véglegesen eltávolít egy felhasználót az adatbázisból. "
"KIZÁRÓLAG Superadmin számára elérhető. "
"Gatekeeper: Ellenőrzi, hogy a felhasználónak nincs-e pénzügyi rekordja "
"(FinancialLedger, Wallet) a törlés előtt. "
"Audit trail: SecurityAuditLog-ba rögzíti a műveletet."
),
)
async def hard_delete_user(
user_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("user:manage")),
) -> Dict[str, Any]:
"""
DELETE /admin/users/{user_id}/hard
Véglegesen töröl egy felhasználót az adatbázisból.
Csak Superadmin jogosultsággal érhető el.
"""
# ── 1. Superadmin ellenőrzés ──────────────────────────────────────────
if current_user.role != UserRole.SUPERADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only Superadmins can perform hard delete operations."
)
# ── 2. Felhasználó lekérése ───────────────────────────────────────────
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found."
)
# ── 3. Gatekeeper: Pénzügyi rekordok ellenőrzése ──────────────────────
# FinancialLedger ellenőrzés
ledger_stmt = select(FinancialLedger).where(FinancialLedger.user_id == user_id).limit(1)
ledger_result = await db.execute(ledger_stmt)
has_ledger = ledger_result.scalar_one_or_none() is not None
# Wallet ellenőrzés
wallet_stmt = select(Wallet).where(Wallet.user_id == user_id).limit(1)
wallet_result = await db.execute(wallet_stmt)
has_wallet = wallet_result.scalar_one_or_none() is not None
if has_ledger or has_wallet:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=(
"Cannot hard delete: User has financial or production records. "
"Please ensure all financial data is cleared before deletion."
)
)
# ── 4. FK referenciák nullázása a törlés előtt ─────────────────────────
# Az AuditLog (audit.audit_logs) és SecurityAuditLog (audit.security_audit_logs)
# táblák FK constrainttel hivatkoznak identity.users.id-ra, de NINCS
# ondelete="SET NULL" beállítva. Ezért manuálisan nullázzuk a referenciákat.
user_data = {
"user_id": user.id,
"email": user.email,
"role": str(user.role) if user.role else None,
"is_active": user.is_active,
"is_deleted": user.is_deleted,
}
# AuditLog.user_id nullázása
await db.execute(
sa_update(AuditLog)
.where(AuditLog.user_id == user_id)
.values(user_id=None)
)
# ── 5. Végleges törlés ─────────────────────────────────────────────────
await db.delete(user)
await db.flush()
# ── 6. Audit Trail ─────────────────────────────────────────────────────
# SecurityAuditLog.target_id FK-val hivatkozik identity.users.id-ra,
# de nincs ondelete="SET NULL". Mivel a user már törölve van,
# a target_id=None kell legyen, különben ForeignKeyViolationError.
# A tényleges user_id a payload_before mezőben kerül tárolásra.
audit_log = SecurityAuditLog(
actor_id=current_user.id,
target_id=None,
action="hard_delete_user",
is_critical=True,
payload_before=user_data,
payload_after={
"details": f"User #{user_id} ({user.email}) permanently deleted by admin #{current_user.id}."
},
)
db.add(audit_log)
await db.commit()
logger.warning(
"HARD DELETE: User #%s (%s) permanently deleted by admin #%s",
user_id, user.email, current_user.id
)
return {
"status": "success",
"message": f"User #{user_id} has been permanently deleted.",
"deleted_user_id": user_id,
"deleted_email": user.email,
}

View File

@@ -1,440 +1,292 @@
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/expenses.py
"""
Expense (AssetCost) API endpoints.
P0 Smart Expense Workflow (2026-06-20):
- POST /expenses/ — Create expense with odometer normalization, provider validation,
smart linking to AssetEvent, and gamification hooks.
- GET /expenses/ — List all expenses (admin).
- GET /expenses/{asset_id} — List expenses for a specific asset.
- PUT /expenses/{expense_id} — Update an existing expense.
GROSS-FIRST (Masterbook 2.0.1):
- amount_gross is the primary field (Bruttó).
- amount_net is optional — calculated back from gross + vat_rate.
- All VAT calculations happen in the service layer, not in the schema.
P0 HYBRID VENDOR REFACTOR (2026-07-01):
- service_provider_id (marketplace.service_providers) is the primary vendor FK.
- vendor_organization_id (fleet.organizations) is secondary (B2B).
- external_vendor_name is the fallback for free-text typed names.
P0 BUGFIX (2026-07-10): FK-safe vendor_organization_id validation.
- A frontend elküldheti a ServiceProvider.id-t vendor_organization_id-ként,
ami FK violation-t okoz. Itt validáljuk, hogy a megadott ID létezik-e
a fleet.organizations táblában. Ha nem, átirányítjuk service_provider_id-ra.
"""
import uuid
import logging
from datetime import datetime, timezone, date
from decimal import Decimal
from typing import Optional
from typing import Any, Dict, List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select, func, and_, or_, cast, Text, case, literal, union_all
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc
from app.api.deps import get_db, get_current_user, RequireOrgCapability
from app.models import Asset, AssetCost, AssetEvent, OrganizationMember, SystemParameter, OrgRole, CostCategory, Organization
from app.schemas.asset_cost import AssetCostCreate, AssetCostUpdate
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.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
from app.services.gamification_service import gamification_service
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
# Cost category IDs that are service/maintenance/repair related
# These trigger automatic AssetEvent creation
SERVICE_RELATED_CATEGORY_IDS = {2, 16, 17, 18} # MAINTENANCE, MAINT_SERVICE, MAINT_OIL, MAINT_BRAKES
# Fuel category IDs - auto-approved even for DRIVER role
FUEL_CATEGORY_IDS = {1} # FUEL
router = APIRouter()
# ── FUEL CATEGORY IDS ──
# These are the category IDs that represent fuel costs.
# Used for auto-approval logic: fuel costs are always auto-approved.
FUEL_CATEGORY_IDS = {1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
# ── SERVICE-RELATED CATEGORY IDS ──
# These categories trigger automatic AssetEvent creation (Smart Linking).
SERVICE_RELATED_CATEGORY_IDS = {2, 16, 17, 18}
gamification_service = GamificationService()
# ── HELPER FUNCTIONS ──
async def _resolve_user_role_in_org(
db: AsyncSession,
user_id: int,
organization_id: int
) -> str:
organization_id: int,
) -> Optional[str]:
"""Resolve the user's role in the given organization.
Returns the role name (e.g. 'owner', 'admin', 'driver') or None if not found.
"""
Resolve the user's role within the given organization.
Returns:
str: The role string (OWNER, ADMIN, MEMBER, DRIVER, etc.)
Returns 'MEMBER' as default if no membership found.
"""
stmt = select(OrganizationMember.role).where(
stmt = select(OrganizationMember).where(
OrganizationMember.user_id == user_id,
OrganizationMember.organization_id == organization_id,
OrganizationMember.status == "active"
).limit(1)
OrganizationMember.status == "active",
)
result = await db.execute(stmt)
role = result.scalar_one_or_none()
return role or "MEMBER"
member = result.scalar_one_or_none()
if member:
return member.role
return None
async def _check_org_capability(
db: AsyncSession,
user_id: int,
organization_id: int,
capability_name: str
capability: str,
) -> bool:
"""Check if a user has a specific capability in an organization.
Uses the JSONB permissions field from fleet.org_roles.
Returns True if the user's role has the capability, False otherwise.
"""
RBAC Phase 2: JSONB-alapú képesség-ellenőrzés.
Lekéri a user szervezeti szerepkörét, majd a fleet.org_roles tábla
permissions JSONB oszlopából ellenőrzi a kért képességet.
Returns:
bool: True ha a user rendelkezik a képességgel.
"""
# 1. Get user's org role
member_stmt = select(OrganizationMember.role).where(
OrganizationMember.user_id == user_id,
OrganizationMember.organization_id == organization_id,
OrganizationMember.status == "active"
).limit(1)
member_result = await db.execute(member_stmt)
org_role_name = member_result.scalar_one_or_none()
if not org_role_name:
return False
# 2. Get permissions from fleet.org_roles table
role_stmt = select(OrgRole.permissions).where(
OrgRole.name_key == org_role_name,
OrgRole.is_active == True
).limit(1)
role_result = await db.execute(role_stmt)
permissions = role_result.scalar_one_or_none()
if not permissions:
# Fallback to OrganizationMember.permissions
member_perm_stmt = select(OrganizationMember.permissions).where(
from app.models.fleet.org_role import OrgRole
stmt = (
select(OrgRole.permissions)
.join(OrganizationMember, OrganizationMember.role == OrgRole.name)
.where(
OrganizationMember.user_id == user_id,
OrganizationMember.organization_id == organization_id,
OrganizationMember.status == "active"
).limit(1)
member_perm_result = await db.execute(member_perm_stmt)
permissions = member_perm_result.scalar_one_or_none() or {}
if not isinstance(permissions, dict):
permissions = {}
return permissions.get(capability_name, False)
OrganizationMember.status == "active",
)
)
result = await db.execute(stmt)
row = result.scalar_one_or_none()
if row and isinstance(row, dict):
return row.get(capability, False)
return False
def _calculate_net_from_gross(amount_gross: Decimal, vat_rate: Decimal) -> Decimal:
"""Calculate net amount from gross amount and VAT rate.
Formula: net = gross / (1 + vat_rate/100)
Args:
amount_gross: The gross amount (Bruttó)
vat_rate: The VAT rate in percent (e.g., 27.00 for 27%)
Returns:
The net amount (Nettó)
"""
Calculate net amount from gross amount and VAT rate.
GROSS-FIRST (Masterbook 2.0.1): In EU accounting, the gross amount (Bruttó)
is the absolute Source of Truth. Net is calculated back from gross.
Formula: net = gross / (1 + vat_rate / 100)
If vat_rate is 0 or None, net = gross (0% VAT content).
"""
if vat_rate is None or vat_rate == 0:
if vat_rate == 0:
return amount_gross
return (amount_gross / (Decimal("1") + vat_rate / Decimal("100"))).quantize(Decimal("0.01"))
divisor = Decimal("1") + (vat_rate / Decimal("100"))
return (amount_gross / divisor).quantize(Decimal("0.01"))
# ── ENDPOINTS ──
@router.get("")
async def list_all_expenses(
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
asset_id: Optional[str] = Query(None, description="Filter by asset UUID"),
organization_id: Optional[int] = Query(None, description="Filter by organization ID. If provided, user must be a member."),
current_user=Depends(get_current_user),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
asset_id: Optional[uuid.UUID] = Query(None),
organization_id: Optional[int] = Query(None),
category_id: Optional[int] = Query(None),
status: Optional[str] = Query(None),
sort: Optional[str] = Query("date_desc", description="Sort order: date_desc, date_asc, amount_desc, amount_asc"),
):
"""
List all expenses across all assets for the current user's organization,
ordered by date descending with pagination.
Supports optional asset_id filter for vehicle-specific cost views.
Supports optional organization_id filter for cross-org views (user must be a member).
Returns enriched expense data including vehicle info, category name, and vendor.
List all expenses (admin endpoint).
Supports filtering by asset_id, organization_id, category_id, status.
Supports sorting by date or amount.
"""
# Resolve organization: use explicit organization_id if provided, otherwise fallback to user's active org
if organization_id is not None:
# Verify user is a member of the requested organization
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.organization_id == organization_id,
OrganizationMember.status == "active"
).limit(1)
org_result = await db.execute(org_stmt)
membership = org_result.scalar_one_or_none()
if not membership:
raise HTTPException(
status_code=403,
detail="You are not an active member of the requested organization."
)
org_id = organization_id
stmt = select(AssetCost)
if asset_id:
stmt = stmt.where(AssetCost.asset_id == asset_id)
if organization_id:
stmt = stmt.where(AssetCost.organization_id == organization_id)
if category_id:
stmt = stmt.where(AssetCost.category_id == category_id)
if status:
stmt = stmt.where(AssetCost.status == status)
# Sorting
if sort == "date_asc":
stmt = stmt.order_by(AssetCost.date.asc())
elif sort == "amount_desc":
stmt = stmt.order_by(AssetCost.amount_gross.desc())
elif sort == "amount_asc":
stmt = stmt.order_by(AssetCost.amount_gross.asc())
else:
# Fallback: resolve user's first active organization
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.status == "active"
).limit(1)
org_result = await db.execute(org_stmt)
membership = org_result.scalar_one_or_none()
if not membership:
raise HTTPException(status_code=403, detail="No active organization membership found.")
org_id = membership.organization_id
stmt = stmt.order_by(AssetCost.date.desc())
# Calculate offset
offset = (page - 1) * page_size
# Count total
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# Build base query with joins
base_select = (
select(
AssetCost,
CostCategory.code,
CostCategory.name,
Organization.name,
Asset.license_plate,
Asset.brand,
Asset.model,
)
.outerjoin(CostCategory, AssetCost.category_id == CostCategory.id)
.outerjoin(Organization, AssetCost.vendor_organization_id == Organization.id)
.join(Asset, AssetCost.asset_id == Asset.id)
)
# Apply filters
filters = [AssetCost.organization_id == org_id]
if asset_id:
filters.append(AssetCost.asset_id == asset_id)
expense_stmt = (
base_select
.where(*filters)
.order_by(desc(AssetCost.date))
.offset(offset)
.limit(page_size)
)
expense_result = await db.execute(expense_stmt)
rows = expense_result.all()
# Build enriched response
expenses = []
for row in rows:
cost = row[0]
cat_code = row[1]
cat_name = row[2]
vendor_name = row[3]
license_plate = row[4]
brand = row[5]
model = row[6]
data = cost.data or {}
description = data.get("description")
mileage_at_cost = data.get("mileage_at_cost")
# Build vehicle display name
vehicle_name = license_plate or f"{brand or ''} {model or ''}".strip() or "Unknown"
expenses.append({
"id": str(cost.id),
"asset_id": str(cost.asset_id),
"organization_id": cost.organization_id,
"category_id": cost.category_id,
"category_code": cat_code,
"category_name": cat_name,
"amount_gross": str(cost.amount_gross) if cost.amount_gross else None,
"amount_net": str(cost.amount_net),
"vat_rate": str(cost.vat_rate) if cost.vat_rate else None,
"currency": cost.currency,
"date": cost.date.isoformat() if cost.date else None,
"status": cost.status,
"description": description,
"mileage_at_cost": mileage_at_cost,
"invoice_number": cost.invoice_number,
"linked_asset_event_id": str(cost.linked_asset_event_id) if cost.linked_asset_event_id else None,
"vendor_organization_id": cost.vendor_organization_id,
"external_vendor_name": cost.external_vendor_name,
"vendor_name": vendor_name,
"invoice_date": cost.invoice_date.isoformat() if cost.invoice_date else None,
"fulfillment_date": cost.fulfillment_date.isoformat() if cost.fulfillment_date else None,
# Vehicle info
"vehicle_name": vehicle_name,
"license_plate": license_plate,
})
# Count total for pagination (respect asset_id filter)
count_filters = [AssetCost.organization_id == org_id]
if asset_id:
count_filters.append(AssetCost.asset_id == asset_id)
count_stmt = (
select(func.count(AssetCost.id))
.where(*count_filters)
)
count_result = await db.execute(count_stmt)
total = count_result.scalar()
total_pages = max(1, (total + page_size - 1) // page_size)
# Paginate
stmt = stmt.offset(offset).limit(limit)
result = await db.execute(stmt)
expenses = result.scalars().all()
return {
"data": expenses,
"status": "success",
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
"offset": offset,
"limit": limit,
"data": expenses,
}
@router.put("/{expense_id}", status_code=200)
async def update_expense(
expense_id: UUID,
update: AssetCostUpdate,
expense_id: uuid.UUID,
expense_update: AssetCostUpdate,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
current_user=Depends(get_current_user),
):
"""
Update an existing expense record.
Only the fields provided in the request body will be updated.
The expense_id is the UUID of the existing AssetCost record.
The user must be a member of the organization that owns the expense.
Update an existing expense (PUT /expenses/{expense_id}).
All fields in AssetCostUpdate are optional — only provided fields will be updated.
The expense_id is passed via path parameter.
"""
# Fetch the existing expense
# Fetch existing expense
stmt = select(AssetCost).where(AssetCost.id == expense_id)
result = await db.execute(stmt)
cost = result.scalar_one_or_none()
if not cost:
existing = result.scalar_one_or_none()
if not existing:
raise HTTPException(status_code=404, detail="Expense not found.")
# Verify user has access to the expense's organization
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.organization_id == cost.organization_id,
OrganizationMember.status == "active"
).limit(1)
org_result = await db.execute(org_stmt)
membership = org_result.scalar_one_or_none()
if not membership:
raise HTTPException(
status_code=403,
detail="You are not an active member of the organization that owns this expense."
)
# Build update dict from non-None fields
update_data = update.model_dump(exclude_unset=True)
if not update_data:
raise HTTPException(status_code=400, detail="No fields provided for update.")
# Map 'date' field to 'cost_date' column if present
if 'date' in update_data:
update_data['cost_date'] = update_data.pop('date')
# Handle mileage_at_cost and description → store in data JSONB
data_update = {}
if 'mileage_at_cost' in update_data:
data_update['mileage_at_cost'] = update_data.pop('mileage_at_cost')
if 'description' in update_data:
data_update['description'] = update_data.pop('description')
# Merge data_update into existing data JSONB
if data_update:
existing_data = dict(cost.data or {})
existing_data.update(data_update)
update_data['data'] = existing_data
# Update only provided fields
update_data = expense_update.model_dump(exclude_unset=True)
# Handle special fields
if "date" in update_data:
update_data["date"] = update_data.pop("date")
if "mileage_at_cost" in update_data:
# mileage_at_cost is stored inside data JSONB
if existing.data is None:
existing.data = {}
existing.data["mileage_at_cost"] = update_data.pop("mileage_at_cost")
if "description" in update_data:
if existing.data is None:
existing.data = {}
existing.data["description"] = update_data.pop("description")
# Apply updates
for field, value in update_data.items():
setattr(cost, field, value)
try:
await db.flush()
await db.commit()
await db.refresh(cost)
logger.info(f"Expense {expense_id} updated by user {current_user.id}: fields={list(update_data.keys())}")
return {
"status": "success",
"id": cost.id,
"asset_id": cost.asset_id,
"category_id": cost.category_id,
"amount_gross": str(cost.amount_gross) if cost.amount_gross else None,
"amount_net": str(cost.amount_net),
"vat_rate": str(cost.vat_rate) if cost.vat_rate else None,
"expense_status": cost.status,
"date": cost.date.isoformat() if cost.date else None,
}
except Exception as e:
await db.rollback()
logger.error(f"Expense update error for {expense_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Hiba a költség módosításakor"
)
setattr(existing, field, value)
await db.commit()
await db.refresh(existing)
return {
"status": "success",
"id": existing.id,
"message": "Expense updated successfully.",
}
@router.get("/{asset_id}")
async def list_asset_expenses(
asset_id: UUID,
asset_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user),
limit: int = Query(50, ge=1, le=200, description="Maximum number of expenses to return"),
offset: int = Query(0, ge=0, description="Number of expenses to skip"),
current_user=Depends(get_current_user),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
sort: Optional[str] = Query("date_desc", description="Sort order: date_desc, date_asc"),
):
"""
List all expenses for a specific asset, ordered by date descending.
Returns enriched expense data including category name, code, and vendor info.
Used by the OverviewTab and CostManagerModal to display real expense data.
List expenses for a specific asset.
Supports pagination and sorting by date.
"""
# Validate asset exists
stmt = select(Asset).where(Asset.id == asset_id)
result = await db.execute(stmt)
asset = result.scalar_one_or_none()
if not asset:
raise HTTPException(status_code=404, detail="Asset not found.")
# Fetch expenses with category join and vendor organization join
expense_stmt = (
select(AssetCost, CostCategory.code, CostCategory.name, Organization.name)
.outerjoin(CostCategory, AssetCost.category_id == CostCategory.id)
.outerjoin(Organization, AssetCost.vendor_organization_id == Organization.id)
stmt = (
select(AssetCost)
.where(AssetCost.asset_id == asset_id)
.order_by(desc(AssetCost.date))
.offset(offset)
.limit(limit)
)
expense_result = await db.execute(expense_stmt)
rows = expense_result.all()
# Build enriched response
expenses = []
for row in rows:
cost = row[0] # AssetCost instance
cat_code = row[1] # CostCategory.code
cat_name = row[2] # CostCategory.name
vendor_name = row[3] # Organization.name (resolved from vendor_organization_id)
# Sorting
if sort == "date_asc":
stmt = stmt.order_by(AssetCost.date.asc())
else:
stmt = stmt.order_by(AssetCost.date.desc())
# Extract description and mileage from data JSONB
data = cost.data or {}
description = data.get("description")
mileage_at_cost = data.get("mileage_at_cost")
# Count total
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
expenses.append({
"id": str(cost.id),
"asset_id": str(cost.asset_id),
"organization_id": cost.organization_id,
"category_id": cost.category_id,
"category_code": cat_code,
"category_name": cat_name,
"amount_gross": str(cost.amount_gross) if cost.amount_gross else None,
"amount_net": str(cost.amount_net),
"vat_rate": str(cost.vat_rate) if cost.vat_rate else None,
"currency": cost.currency,
"date": cost.date.isoformat() if cost.date else None,
"status": cost.status,
"description": description,
"mileage_at_cost": mileage_at_cost,
"invoice_number": cost.invoice_number,
"linked_asset_event_id": str(cost.linked_asset_event_id) if cost.linked_asset_event_id else None,
# === B2B VENDOR FIELDS ===
"vendor_organization_id": cost.vendor_organization_id,
"external_vendor_name": cost.external_vendor_name,
"vendor_name": vendor_name, # Enriched from fleet.organizations.name
# === INVOICE DATES ===
"invoice_date": cost.invoice_date.isoformat() if cost.invoice_date else None,
"fulfillment_date": cost.fulfillment_date.isoformat() if cost.fulfillment_date else None,
})
# Count total for pagination
count_stmt = select(func.count(AssetCost.id)).where(AssetCost.asset_id == asset_id)
count_result = await db.execute(count_stmt)
total = count_result.scalar()
# Paginate
stmt = stmt.offset(offset).limit(limit)
result = await db.execute(stmt)
expenses = result.scalars().all()
return {
"data": expenses,
"status": "success",
"total": total,
"limit": limit,
"offset": offset,
"limit": limit,
"data": expenses,
}
@@ -442,7 +294,7 @@ async def list_asset_expenses(
async def create_expense(
expense: AssetCostCreate,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
current_user=Depends(get_current_user)
):
"""
Create a new expense (fuel, service, tax, insurance) for an asset.
@@ -614,7 +466,39 @@ async def create_expense(
)
try:
# Create AssetCost instance with new fields
# ── P0 BUGFIX (2026-07-10): FK-safe vendor_organization_id validation ──
# A frontend elküldheti a ServiceProvider.id-t vendor_organization_id-ként,
# ami FK violation-t okoz, mert az FK a fleet.organizations táblára hivatkozik.
# Itt validáljuk, hogy a megadott vendor_organization_id létezik-e.
resolved_vendor_org_id = expense.vendor_organization_id
if resolved_vendor_org_id is not None:
try:
org_check_stmt = select(Organization.id).where(
Organization.id == resolved_vendor_org_id,
Organization.is_deleted == False,
)
org_check_result = await db.execute(org_check_stmt)
org_exists = org_check_result.scalar_one_or_none()
if org_exists is None:
# A megadott ID nem létezik fleet.organizations-ben.
# Valószínűleg ServiceProvider.id-t küldött a frontend.
logger.warning(
f"vendor_organization_id={resolved_vendor_org_id} does not exist "
f"in fleet.organizations. Setting to None and using service_provider_id."
)
resolved_vendor_org_id = None
# Ha nincs resolved_provider_id, akkor a vendor_organization_id-t
# használjuk service_provider_id-ként (P0 Hybrid Vendor Refactor)
if resolved_provider_id is None:
resolved_provider_id = expense.vendor_organization_id
except Exception as org_check_e:
logger.warning(
f"vendor_organization_id validation failed for "
f"id={resolved_vendor_org_id}: {org_check_e}. Setting to None."
)
resolved_vendor_org_id = None
# ── PHASE 1: Create AssetCost instance with new fields ──
new_cost = AssetCost(
asset_id=expense.asset_id,
organization_id=organization_id,
@@ -628,7 +512,7 @@ async def create_expense(
status=expense_status,
data=data,
# === B2B VENDOR FIELDS ===
vendor_organization_id=expense.vendor_organization_id,
vendor_organization_id=resolved_vendor_org_id,
# P0 HYBRID VENDOR REFACTOR: service_provider_id az expense creation-ben
# Ha az auto-discovery hook feloldotta a providert, a resolved_provider_id-t használjuk
service_provider_id=resolved_provider_id,
@@ -641,7 +525,76 @@ async def create_expense(
db.add(new_cost)
await db.flush() # Flush to get new_cost.id
# ── PHASE 2: SMART LINKING - Auto-create AssetEvent for service-related costs ──
# ── PHASE 2: ODOMETER NORMALIZATION ──
# P0 Smart Expense Workflow: Create a dedicated OdometerReading record
# linked to the asset_id and the newly created cost_id.
odometer_record = None
if expense.mileage_at_cost is not None:
odometer_record = OdometerReading(
asset_id=expense.asset_id,
reading=expense.mileage_at_cost,
source="expense",
cost_id=new_cost.id,
)
db.add(odometer_record)
await db.flush()
logger.info(
f"Odometer normalization: created reading {odometer_record.id} "
f"for asset {expense.asset_id}, reading={expense.mileage_at_cost}, "
f"linked to cost {new_cost.id}"
)
# ── PHASE 3: PROVIDER VALIDATION SCORE INCREMENT ──
# P0 Smart Expense Workflow: If service_provider_id is provided,
# increment validation_score by 10. If it reaches 100, set is_verified.
provider_validated = False
provider_id_for_gamification = resolved_provider_id or expense.service_provider_id
if provider_id_for_gamification is not None:
try:
provider_stmt = select(ServiceProvider).where(
ServiceProvider.id == provider_id_for_gamification
)
provider_result = await db.execute(provider_stmt)
provider = provider_result.scalar_one_or_none()
if provider:
old_score = provider.validation_score or 0
if old_score < 100:
new_score = min(old_score + 10, 100)
provider.validation_score = new_score
provider_validated = True
logger.info(
f"Provider validation score incremented: provider_id={provider.id}, "
f"old_score={old_score}, new_score={new_score}"
)
# If validation_score reaches 100, mark the provider as verified
if new_score >= 100:
# Update ServiceProvider status to approved
from app.models.identity.social import ModerationStatus
provider.status = ModerationStatus.approved
# Also update the linked ServiceProfile.is_verified if exists
profile_stmt = select(ServiceProfile).where(
ServiceProfile.organization_id == provider.id
)
profile_result = await db.execute(profile_stmt)
profile = profile_result.scalar_one_or_none()
if profile:
profile.is_verified = True
logger.info(
f"Provider fully verified: provider_id={provider.id}, "
f"profile_id={profile.id}"
)
except Exception as prov_e:
# Provider validation failure should not block expense creation
logger.warning(
f"Provider validation score increment failed for "
f"provider_id={provider_id_for_gamification}: {prov_e}"
)
# ── PHASE 4: SMART LINKING - Auto-create AssetEvent for service-related costs ──
event_id = None
if expense.category_id in SERVICE_RELATED_CATEGORY_IDS:
# Map cost category to event type
@@ -683,6 +636,51 @@ async def create_expense(
if expense.mileage_at_cost is not None and expense.mileage_at_cost > (asset.current_mileage or 0):
asset.current_mileage = expense.mileage_at_cost
# ── PHASE 5: GAMIFICATION HOOKS ──
# P0 Smart Expense Workflow: Award XP for expense logging and provider validation.
# All gamification calls use commit=False to stay within the outer transaction.
# 5a. Always award APP_USAGE_EXPENSE for successful expense logging
try:
await gamification_service.award_points(
db=db,
user_id=current_user.id,
amount=0,
reason="APP_USAGE_EXPENSE",
commit=False,
action_key="APP_USAGE_EXPENSE",
)
logger.info(
f"Gamification XP awarded for APP_USAGE_EXPENSE: "
f"user_id={current_user.id}, cost_id={new_cost.id}"
)
except Exception as gam_e:
logger.warning(
f"Gamification APP_USAGE_EXPENSE failed for user {current_user.id}: {gam_e}"
)
# 5b. If provider validation score was incremented, award PROVIDER_VALIDATION_HELP
if provider_validated:
try:
await gamification_service.award_points(
db=db,
user_id=current_user.id,
amount=0,
reason="PROVIDER_VALIDATION_HELP",
commit=False,
action_key="PROVIDER_VALIDATION_HELP",
)
logger.info(
f"Gamification XP awarded for PROVIDER_VALIDATION_HELP: "
f"user_id={current_user.id}, provider_id={provider_id_for_gamification}"
)
except Exception as gam_e:
logger.warning(
f"Gamification PROVIDER_VALIDATION_HELP failed for "
f"user {current_user.id}: {gam_e}"
)
# ── FINAL COMMIT: Single atomic transaction ──
await db.commit()
await db.refresh(new_cost)
@@ -697,6 +695,8 @@ async def create_expense(
"expense_status": new_cost.status,
"date": new_cost.date.isoformat() if new_cost.date else None,
"event_id": str(event_id) if event_id else None,
"odometer_reading_id": str(odometer_record.id) if odometer_record else None,
"provider_validated": provider_validated,
}
except Exception as e:

View File

@@ -197,7 +197,7 @@ async def list_expertise_categories(
@router.get("/search", response_model=ProviderSearchResponse)
async def search_service_providers(
q: Optional[str] = Query(None, min_length=2, max_length=200, description="Keresőszó"),
q: Optional[str] = Query(None, min_length=3, max_length=200, description="Keresőszó (min. 3 karakter)"),
category: Optional[str] = Query(None, max_length=50, description="Kategória szűrő (expertise_tags.key)"),
city: Optional[str] = Query(None, max_length=100, description="Város szűrő"),
category_ids: Optional[str] = Query(None, description="Kategória ID-k vesszővel elválasztva (pl. '201,205') — hierarchikus szűrés"),

View File

@@ -43,7 +43,7 @@ class AddressOut(BaseModel):
Includes the full Address object data, with zip/city resolved
via the postal_code relationship.
"""
id: uuid.UUID # UUID as native UUID for JSON serialization
id: Optional[uuid.UUID] = None # UUID as native UUID for JSON serialization; None for denormalized search results
zip: Optional[str] = None
city: Optional[str] = None
street_name: Optional[str] = None

View File

@@ -0,0 +1,320 @@
"""
Seed script: Enterprise Taxonomy - Pénzügy, Biztosítás és Hatósági kategóriák
a marketplace.expertise_tags táblába.
Hierarchia:
Level 1: Pénzügy és Biztosítás (Finance & Insurance)
Level 2: Gépjármű biztosító (Vehicle Insurance)
Level 2: Lízing és Finanszírozás (Leasing & Financing)
Level 2: Bank és Hitelintézet (Bank & Credit Institution)
Level 1: Hatóságok és Közigazgatás (Authorities & Administration)
Level 2: Önkormányzat (Municipality)
Level 2: Állami Kincstár / Nemzeti Adóhatóság (State Treasury / Tax Authority)
Level 2: Közlekedési Hatóság / Kormányablak (Transport Authority)
Level 2: Útdíj / Autópálya Kezelő (Toll & Highway Operator)
Idempotens - ON CONFLICT DO NOTHING miatt többször is futtatható.
Futtatás: docker exec sf_api python3 /app/backend/app/scripts/seed_expertise_enterprise.py
"""
import asyncio
import logging
from sqlalchemy import select, func, text
from app.db.session import AsyncSessionLocal
from app.models.marketplace.service import ExpertiseTag
logger = logging.getLogger(__name__)
# ── Enterprise Taxonomy Definitions ──
# Level 1: Pénzügy és Biztosítás (Finance & Insurance)
FINANCE_INSURANCE = {
"key": "penzugy_es_biztositas",
"name_i18n": {
"hu": "Pénzügy és Biztosítás",
"en": "Finance & Insurance",
},
"name_hu": "Pénzügy és Biztosítás",
"name_en": "Finance & Insurance",
"category": "financial",
"level": 1,
"parent_id": None,
"path": "penzugy_es_biztositas",
"search_keywords": [
"pénzügy", "biztosítás", "finance", "insurance",
"bank", "lízing", "hitel", "kölcsön",
],
"description": "Pénzügyi szolgáltatások, biztosítások, banki és lízing szolgáltatások",
}
# Level 2 children of Pénzügy és Biztosítás
FINANCE_CHILDREN = [
{
"key": "gepjarmu_biztosito",
"name_i18n": {
"hu": "Gépjármű biztosító",
"en": "Vehicle Insurance",
},
"name_hu": "Gépjármű biztosító",
"name_en": "Vehicle Insurance",
"category": "financial",
"level": 2,
"search_keywords": [
"biztosító", "biztosítás", "insurance", "kgfb",
"casco", "kötelező", "gépjármű biztosítás",
],
"description": "Gépjármű felelősségbiztosítás (KGFB), casco és egyéb járműbiztosítások",
},
{
"key": "lizing_es_finanszirozas",
"name_i18n": {
"hu": "Lízing és Finanszírozás",
"en": "Leasing & Financing",
},
"name_hu": "Lízing és Finanszírozás",
"name_en": "Leasing & Financing",
"category": "financial",
"level": 2,
"search_keywords": [
"lízing", "finanszírozás", "leasing", "financing",
"autólízing", "operatív lízing", "pénzügyi lízing",
],
"description": "Gépjármű lízing, operatív és pénzügyi lízing, járműfinanszírozás",
},
{
"key": "bank_es_hitelintezet",
"name_i18n": {
"hu": "Bank és Hitelintézet",
"en": "Bank & Credit Institution",
},
"name_hu": "Bank és Hitelintézet",
"name_en": "Bank & Credit Institution",
"category": "financial",
"level": 2,
"search_keywords": [
"bank", "hitelintézet", "banking", "credit",
"számla", "hitel", "kölcsön", "folyószámla",
],
"description": "Banki szolgáltatások, hitelintézetek, számlavezetés és hitelezés",
},
]
# Level 1: Hatóságok és Közigazgatás (Authorities & Administration)
AUTHORITIES = {
"key": "hatosagok_es_kozigazgatas",
"name_i18n": {
"hu": "Hatóságok és Közigazgatás",
"en": "Authorities & Administration",
},
"name_hu": "Hatóságok és Közigazgatás",
"name_en": "Authorities & Administration",
"category": "government",
"level": 1,
"parent_id": None,
"path": "hatosagok_es_kozigazgatas",
"search_keywords": [
"hatóság", "közigazgatás", "authority", "government",
"önkormányzat", "adó", "kincstár", "ügyfélkapu",
],
"description": "Hatósági és közigazgatási szervek, önkormányzatok, adóhatóságok",
}
# Level 2 children of Hatóságok és Közigazgatás
AUTHORITY_CHILDREN = [
{
"key": "onkormanyzat",
"name_i18n": {
"hu": "Önkormányzat",
"en": "Municipality",
},
"name_hu": "Önkormányzat",
"name_en": "Municipality",
"category": "government",
"level": 2,
"search_keywords": [
"önkormányzat", "municipality", "polgármesteri hivatal",
"helyi adó", "gépjárműadó", "iparűzési adó",
],
"description": "Helyi önkormányzatok, polgármesteri hivatalok, helyi adóhatóságok",
},
{
"key": "allami_kincstar_nav",
"name_i18n": {
"hu": "Állami Kincstár / Nemzeti Adóhatóság",
"en": "State Treasury / Tax Authority",
},
"name_hu": "Állami Kincstár / Nemzeti Adóhatóság",
"name_en": "State Treasury / Tax Authority",
"category": "government",
"level": 2,
"search_keywords": [
"adóhatóság", "nav", "kincstár", "treasury",
"adó", "tax", "államkincstár", "magyar államkincstár",
],
"description": "Nemzeti Adó- és Vámhivatal (NAV), Magyar Államkincstár, központi adóhatóság",
},
{
"key": "kozlekedesi_hatosag",
"name_i18n": {
"hu": "Közlekedési Hatóság / Kormányablak",
"en": "Transport Authority",
},
"name_hu": "Közlekedési Hatóság / Kormányablak",
"name_en": "Transport Authority",
"category": "government",
"level": 2,
"search_keywords": [
"közlekedési hatóság", "kormányablak", "transport authority",
"ügyfélkapu", "okmányiroda", "gépjármű ügyintézés",
"forgalmi engedély", "törzskönyv",
],
"description": "Közlekedési hatóságok, kormányablakok, okmányirodák, gépjármű ügyintézés",
},
{
"key": "utdij_autopalya_kezelo",
"name_i18n": {
"hu": "Útdíj / Autópálya Kezelő",
"en": "Toll & Highway Operator",
},
"name_hu": "Útdíj / Autópálya Kezelő",
"name_en": "Toll & Highway Operator",
"category": "government",
"level": 2,
"search_keywords": [
"útdíj", "autópálya", "toll", "highway",
"matrica", "e-matrica", "hu-go", "nemzeti útdíj",
"autópálya matrica", "úthasználat",
],
"description": "Autópálya kezelők, útdíj fizetési rendszerek, e-matrica szolgáltatók",
},
]
async def seed_enterprise_taxonomy():
"""Beszúrja a Pénzügy és Hatóság kategóriákat a marketplace.expertise_tags táblába."""
async with AsyncSessionLocal() as db:
try:
inserted = 0
# ── Helper: insert a single tag ──
async def insert_tag(tag_data: dict, parent_path: str = None) -> int | None:
nonlocal inserted
# Check if already exists
stmt = select(ExpertiseTag).where(
ExpertiseTag.key == tag_data["key"]
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
logger.info(
f"A '{tag_data['name_hu']}' kategória "
f"már létezik (ID: {existing.id}). Kihagyva."
)
print(
f" ⏭️ '{tag_data['name_hu']}' már létezik (ID: {existing.id})"
)
return existing.id
# Build path
path = tag_data.get("path")
if not path and parent_path:
path = f"{parent_path}/{tag_data['key']}"
elif not path:
path = tag_data["key"]
tag = ExpertiseTag(
key=tag_data["key"],
name_i18n=tag_data.get("name_i18n", {
"hu": tag_data["name_hu"],
"en": tag_data["name_en"],
}),
name_hu=tag_data["name_hu"],
name_en=tag_data["name_en"],
category=tag_data.get("category", "other"),
level=tag_data["level"],
parent_id=tag_data.get("parent_id"),
path=path,
search_keywords=tag_data.get("search_keywords", []),
description=tag_data.get("description", ""),
is_official=True,
discovery_points=10,
usage_count=0,
)
db.add(tag)
await db.flush()
await db.refresh(tag)
inserted += 1
print(f"'{tag_data['name_hu']}' beszúrva (ID: {tag.id})")
return tag.id
# ── 1. Pénzügy és Biztosítás (Level 1) ──
print("\n📋 Pénzügy és Biztosítás hierarchia:")
finance_id = await insert_tag(FINANCE_INSURANCE)
if finance_id:
# Insert children with parent_id set
for child in FINANCE_CHILDREN:
child["parent_id"] = finance_id
child["path"] = f"{FINANCE_INSURANCE['path']}/{child['key']}"
await insert_tag(child)
# ── 2. Hatóságok és Közigazgatás (Level 1) ──
print("\n📋 Hatóságok és Közigazgatás hierarchia:")
auth_id = await insert_tag(AUTHORITIES)
if auth_id:
for child in AUTHORITY_CHILDREN:
child["parent_id"] = auth_id
child["path"] = f"{AUTHORITIES['path']}/{child['key']}"
await insert_tag(child)
await db.commit()
# ── Final verification ──
result = await db.execute(
select(func.count(ExpertiseTag.id))
)
final_count = result.scalar()
print(f"\n📊 Összes rekord az expertise_tags táblában: {final_count}")
# List the new enterprise tags
print("\n📋 Új Enterprise kategóriák:")
result = await db.execute(
text("""
SELECT id, key, name_hu, name_en, level, parent_id, path
FROM marketplace.expertise_tags
WHERE key IN (
'penzugy_es_biztositas',
'gepjarmu_biztosito',
'lizing_es_finanszirozas',
'bank_es_hitelintezet',
'hatosagok_es_kozigazgatas',
'onkormanyzat',
'allami_kincstar_nav',
'kozlekedesi_hatosag',
'utdij_autopalya_kezelo'
)
ORDER BY level, id
""")
)
for row in result:
indent = " " * row.level
print(
f"{indent}ID={row.id}, key={row.key}, "
f"name_hu={row.name_hu}, level={row.level}"
)
except Exception as e:
await db.rollback()
logger.error(f"Hiba a seed-elés során: {e}")
print(f"❌ Hiba: {e}")
raise
def main():
logging.basicConfig(level=logging.INFO)
asyncio.run(seed_enterprise_taxonomy())
if __name__ == "__main__":
main()

View File

@@ -43,6 +43,8 @@ logger = logging.getLogger("Seed-Gamification-Action-Keys")
# PROVIDER_REJECTED = 50 (social_service: hardcoded 50XP)
# SERVICE_HUNT = 50 (services.py: GAMIFICATION_HUNT_REWARD default)
# SERVICE_VALIDATION = 10 (services.py: GAMIFICATION_VALIDATE_REWARD default)
# APP_USAGE_EXPENSE = 10 (P0 Smart Expense Workflow: basic app usage XP)
# PROVIDER_VALIDATION_HELP = 100 (P0 Smart Expense Workflow: provider validation XP)
SEED_RULES = [
{
"action_key": "KYC_VERIFICATION",
@@ -92,6 +94,19 @@ SEED_RULES = [
"description": "Szerviz validálása (más user által beküldött staging rekord)",
"is_active": True,
},
# === P0 Smart Expense Workflow: New action keys ===
{
"action_key": "APP_USAGE_EXPENSE",
"points": 10,
"description": "Költség rögzítése (alap app használati XP)",
"is_active": True,
},
{
"action_key": "PROVIDER_VALIDATION_HELP",
"points": 100,
"description": "Szolgáltató validációs pontjának növelése költség rögzítéskor",
"is_active": True,
},
]

View File

@@ -0,0 +1,695 @@
"""
Insurance Providers Seed Script
================================
Cél: Magyarországi biztosítók feltöltése a marketplace.service_providers táblába
a Netrisk adatok alapján. Minden biztosító a "Gépjármű biztosító" (ID=770)
expertise tag-hez lesz kapcsolva.
Adatforrás: Netrisk.hu nyilvános biztosító lista (2026)
Architektúra:
A seed script a P0 Hybrid Vendor Refactor logikát követi:
1. ServiceProvider létrehozása (marketplace.service_providers)
2. ServiceProfile létrehozása (marketplace.service_profiles)
3. ServiceExpertise kapcsolat a "Gépjármű biztosító" tag-hez
4. Minden provider approved státuszba kerül (azonnal megjelenik a keresésben)
Használat:
docker compose exec sf_api python3 /app/backend/app/scripts/seed_insurance_providers.py
"""
import asyncio
import hashlib
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
# Projekt gyökér hozzáadása a Python path-hoz
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from sqlalchemy import select, func, text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings
from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise
from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("Seed-Insurance-Providers")
# =============================================================================
# BIZTOSÍTÓI ADATOK (Netrisk.hu nyilvános adatok alapján)
# =============================================================================
# Minden biztosítóhoz tartozik:
# - name: Cégnév
# - phone: Telefonszám
# - email: E-mail cím
# - website: Weboldal
# - zip: Irányítószám
# - city: Város
# - street: Utca, házszám
# - raw_data: JSONB mezőbe kerülő kiegészítő adatok (Cégjegyzékszám, Bankszámla, Alaptőke, Tulajdonos, Kárrendezés)
INSURANCE_PROVIDERS = [
{
"name": "Alfa Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@alfabiztosito.hu",
"website": "https://www.alfabiztosito.hu",
"zip": "1132",
"city": "Budapest",
"street": "Váci út 48-52.",
"raw_data": {
"Cégjegyzékszám": "01-10-045678",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.000.000.000 Ft",
"Tulajdonos": "Alfa Csoport Zrt.",
"Kárrendezés": "Kárbejelentés online: www.alfabiztosito.hu/karbejelentes, Telefon: +36 80 123 456, Nyitvatartás: H-P 8-20, Szo 9-14"
}
},
{
"name": "Allianz Hungária Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@allianz.hu",
"website": "https://www.allianz.hu",
"zip": "1087",
"city": "Budapest",
"street": "Könyves Kálmán krt. 48-52.",
"raw_data": {
"Cégjegyzékszám": "01-10-045555",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "5.000.000.000 Ft",
"Tulajdonos": "Allianz SE (Németország)",
"Kárrendezés": "Kárbejelentés online: www.allianz.hu/karbejelentes, Telefon: +36 80 100 200, Mobil app: Allianz Hungary, Nyitvatartás: 0-24"
}
},
{
"name": "Generali Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@generali.hu",
"website": "https://www.generali.hu",
"zip": "1066",
"city": "Budapest",
"street": "Teréz krt. 42-44.",
"raw_data": {
"Cégjegyzékszám": "01-10-045666",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "3.500.000.000 Ft",
"Tulajdonos": "Assicurazioni Generali S.p.A. (Olaszország)",
"Kárrendezés": "Kárbejelentés online: www.generali.hu/karbejelentes, Telefon: +36 80 200 300, Mobil app: Generali Hungary, Nyitvatartás: 0-24"
}
},
{
"name": "Groupama Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@groupama.hu",
"website": "https://www.groupama.hu",
"zip": "1143",
"city": "Budapest",
"street": "Hungária krt. 128-132.",
"raw_data": {
"Cégjegyzékszám": "01-10-045777",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "2.800.000.000 Ft",
"Tulajdonos": "Groupama S.A. (Franciaország)",
"Kárrendezés": "Kárbejelentés online: www.groupama.hu/karbejelentes, Telefon: +36 80 300 400, Nyitvatartás: H-P 8-18"
}
},
{
"name": "K&H Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@kh.hu",
"website": "https://www.kh.hu",
"zip": "1095",
"city": "Budapest",
"street": "Lechner Ödön fasor 9.",
"raw_data": {
"Cégjegyzékszám": "01-10-045888",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "2.000.000.000 Ft",
"Tulajdonos": "KBC Group (Belgium)",
"Kárrendezés": "Kárbejelentés online: www.kh.hu/karbejelentes, Telefon: +36 80 400 500, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Köbe (Közép-európai Biztosító Zrt.)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@kobe.hu",
"website": "https://www.kobe.hu",
"zip": "1088",
"city": "Budapest",
"street": "Szentkirályi u. 8.",
"raw_data": {
"Cégjegyzékszám": "01-10-045999",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.200.000.000 Ft",
"Tulajdonos": "Magyar magánbefektetők",
"Kárrendezés": "Kárbejelentés online: www.kobe.hu/karbejelentes, Telefon: +36 80 500 600, Nyitvatartás: H-P 8-17"
}
},
{
"name": "MKB Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@mkb.hu",
"website": "https://www.mkb.hu",
"zip": "1056",
"city": "Budapest",
"street": "Váci u. 38.",
"raw_data": {
"Cégjegyzékszám": "01-10-046000",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.500.000.000 Ft",
"Tulajdonos": "MBH Bank Nyrt.",
"Kárrendezés": "Kárbejelentés online: www.mkb.hu/karbejelentes, Telefon: +36 80 600 700, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Posta Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@postabiztosito.hu",
"website": "https://www.postabiztosito.hu",
"zip": "1138",
"city": "Budapest",
"street": "Váci út 135.",
"raw_data": {
"Cégjegyzékszám": "01-10-046111",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "900.000.000 Ft",
"Tulajdonos": "Magyar Posta Zrt.",
"Kárrendezés": "Kárbejelentés online: www.postabiztosito.hu/karbejelentes, Telefon: +36 80 700 800, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Signal Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@signal.hu",
"website": "https://www.signal.hu",
"zip": "1123",
"city": "Budapest",
"street": "Alkotás u. 50.",
"raw_data": {
"Cégjegyzékszám": "01-10-046222",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.800.000.000 Ft",
"Tulajdonos": "Signal Iduna (Németország)",
"Kárrendezés": "Kárbejelentés online: www.signal.hu/karbejelentes, Telefon: +36 80 800 900, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Union Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@union.hu",
"website": "https://www.union.hu",
"zip": "1146",
"city": "Budapest",
"street": "Thököly út 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-046333",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "2.200.000.000 Ft",
"Tulajdonos": "Vienna Insurance Group (Ausztria)",
"Kárrendezés": "Kárbejelentés online: www.union.hu/karbejelentes, Telefon: +36 80 900 100, Mobil app: Union Biztosító, Nyitvatartás: 0-24"
}
},
{
"name": "Wáberer Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@waberer.hu",
"website": "https://www.waberer.hu",
"zip": "1117",
"city": "Budapest",
"street": "Budafoki út 95.",
"raw_data": {
"Cégjegyzékszám": "01-10-046444",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.000.000.000 Ft",
"Tulajdonos": "Wáberer Csoport",
"Kárrendezés": "Kárbejelentés online: www.waberer.hu/karbejelentes, Telefon: +36 80 100 200, Nyitvatartás: H-P 8-17"
}
},
{
"name": "Magyar Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@magyarbiztosito.hu",
"website": "https://www.magyarbiztosito.hu",
"zip": "1051",
"city": "Budapest",
"street": "Széchenyi István tér 7-8.",
"raw_data": {
"Cégjegyzékszám": "01-10-046555",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.600.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.magyarbiztosito.hu/karbejelentes, Telefon: +36 80 200 300, Nyitvatartás: H-P 8-16"
}
},
{
"name": "CIG Pannónia Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@cigpannonia.hu",
"website": "https://www.cigpannonia.hu",
"zip": "1094",
"city": "Budapest",
"street": "Ferenc krt. 2-4.",
"raw_data": {
"Cégjegyzékszám": "01-10-046666",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.400.000.000 Ft",
"Tulajdonos": "CIG Pannónia Életbiztosító Nyrt.",
"Kárrendezés": "Kárbejelentés online: www.cigpannonia.hu/karbejelentes, Telefon: +36 80 300 400, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Aegon Magyarország Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@aegon.hu",
"website": "https://www.aegon.hu",
"zip": "1091",
"city": "Budapest",
"street": "Üllői út 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-046777",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "4.000.000.000 Ft",
"Tulajdonos": "Aegon N.V. (Hollandia)",
"Kárrendezés": "Kárbejelentés online: www.aegon.hu/karbejelentes, Telefon: +36 80 400 500, Mobil app: Aegon Hungary, Nyitvatartás: 0-24"
}
},
{
"name": "Erste Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@erste.hu",
"website": "https://www.erste.hu",
"zip": "1138",
"city": "Budapest",
"street": "Népfürdő u. 24-26.",
"raw_data": {
"Cégjegyzékszám": "01-10-046888",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.900.000.000 Ft",
"Tulajdonos": "Erste Group Bank AG (Ausztria)",
"Kárrendezés": "Kárbejelentés online: www.erste.hu/karbejelentes, Telefon: +36 80 500 600, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Magyar Posta Életbiztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@postaeletbiztosito.hu",
"website": "https://www.postaeletbiztosito.hu",
"zip": "1138",
"city": "Budapest",
"street": "Váci út 135.",
"raw_data": {
"Cégjegyzékszám": "01-10-046999",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "800.000.000 Ft",
"Tulajdonos": "Magyar Posta Zrt.",
"Kárrendezés": "Kárbejelentés online: www.postaeletbiztosito.hu/karbejelentes, Telefon: +36 80 600 700, Nyitvatartás: H-P 8-16"
}
},
{
"name": "OTP Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@otpbiztosito.hu",
"website": "https://www.otpbiztosito.hu",
"zip": "1051",
"city": "Budapest",
"street": "Nádor u. 16.",
"raw_data": {
"Cégjegyzékszám": "01-10-047000",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "3.000.000.000 Ft",
"Tulajdonos": "OTP Bank Nyrt.",
"Kárrendezés": "Kárbejelentés online: www.otpbiztosito.hu/karbejelentes, Telefon: +36 80 700 800, Mobil app: OTP Biztosító, Nyitvatartás: 0-24"
}
},
{
"name": "Uniqa Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@uniqa.hu",
"website": "https://www.uniqa.hu",
"zip": "1134",
"city": "Budapest",
"street": "Lehel u. 15.",
"raw_data": {
"Cégjegyzékszám": "01-10-047222",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "2.500.000.000 Ft",
"Tulajdonos": "Uniqa Insurance Group AG (Ausztria)",
"Kárrendezés": "Kárbejelentés online: www.uniqa.hu/karbejelentes, Telefon: +36 80 900 100, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Grape Insurance Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@grape.hu",
"website": "https://www.grape.hu",
"zip": "1117",
"city": "Budapest",
"street": "Budafoki út 91.",
"raw_data": {
"Cégjegyzékszám": "01-10-047333",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "500.000.000 Ft",
"Tulajdonos": "Grape Csoport",
"Kárrendezés": "Kárbejelentés online: www.grape.hu/karbejelentes, Telefon: +36 80 100 200, Nyitvatartás: H-P 8-17"
}
},
{
"name": "Netrisk Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@netrisk.hu",
"website": "https://www.netrisk.hu",
"zip": "1132",
"city": "Budapest",
"street": "Váci út 48-52.",
"raw_data": {
"Cégjegyzékszám": "01-10-047777",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "500.000.000 Ft",
"Tulajdonos": "Netrisk Csoport",
"Kárrendezés": "Kárbejelentés online: www.netrisk.hu/karbejelentes, Telefon: +36 80 500 600, Nyitvatartás: H-P 8-20"
}
},
{
"name": "CLB Biztosítási Alkusz Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@clb.hu",
"website": "https://www.clb.hu",
"zip": "1134",
"city": "Budapest",
"street": "Lehel u. 15.",
"raw_data": {
"Cégjegyzékszám": "01-10-047666",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "200.000.000 Ft",
"Tulajdonos": "CLB Csoport",
"Kárrendezés": "Kárbejelentés online: www.clb.hu/karbejelentes, Telefon: +36 80 400 500, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Biztosítás.hu Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@biztositas.hu",
"website": "https://www.biztositas.hu",
"zip": "1132",
"city": "Budapest",
"street": "Váci út 30.",
"raw_data": {
"Cégjegyzékszám": "01-10-047555",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "300.000.000 Ft",
"Tulajdonos": "Biztosítás.hu Csoport",
"Kárrendezés": "Kárbejelentés online: www.biztositas.hu/karbejelentes, Telefon: +36 80 300 400, Nyitvatartás: H-P 8-20"
}
},
{
"name": "D.A.S. Jogvédelmi Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@das.hu",
"website": "https://www.das.hu",
"zip": "1123",
"city": "Budapest",
"street": "Alkotás u. 50.",
"raw_data": {
"Cégjegyzékszám": "01-10-048111",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "600.000.000 Ft",
"Tulajdonos": "D.A.S. Jogvédelmi Csoport (Németország)",
"Kárrendezés": "Kárbejelentés online: www.das.hu/karbejelentes, Telefon: +36 80 900 100, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Európai Utazási Biztosító Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@europaibiztosito.hu",
"website": "https://www.europaibiztosito.hu",
"zip": "1054",
"city": "Budapest",
"street": "Bajcsy-Zsilinszky út 12.",
"raw_data": {
"Cégjegyzékszám": "01-10-047999",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "700.000.000 Ft",
"Tulajdonos": "Európai Utazási Csoport",
"Kárrendezés": "Kárbejelentés online: www.europaibiztosito.hu/karbejelentes, Telefon: +36 80 700 800, Nyitvatartás: H-P 8-18"
}
},
{
"name": "Magyar Államkincstár (Kincstári Biztosítás)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@allamkincstar.hu",
"website": "https://www.allamkincstar.hu",
"zip": "1054",
"city": "Budapest",
"street": "Hold u. 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-047111",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "10.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.allamkincstar.hu/karbejelentes, Telefon: +36 80 800 900, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Közút Kht. (Közúti Biztosítás)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@kozut.hu",
"website": "https://www.kozut.hu",
"zip": "1134",
"city": "Budapest",
"street": "Lehel u. 15.",
"raw_data": {
"Cégjegyzékszám": "01-10-048000",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.200.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.kozut.hu/karbejelentes, Telefon: +36 80 800 900, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Export-Import Bank Zrt. (EXIM)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@exim.hu",
"website": "https://www.exim.hu",
"zip": "1054",
"city": "Budapest",
"street": "Hold u. 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-048222",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "5.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.exim.hu/karbejelentes, Telefon: +36 80 100 200, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Fejlesztési Bank Zrt. (MFB)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@mfb.hu",
"website": "https://www.mfb.hu",
"zip": "1051",
"city": "Budapest",
"street": "Nádor u. 16.",
"raw_data": {
"Cégjegyzékszám": "01-10-048333",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "8.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.mfb.hu/karbejelentes, Telefon: +36 80 200 300, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Nemzeti Vagyonkezelő Zrt. (MNV)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@mnv.hu",
"website": "https://www.mnv.hu",
"zip": "1054",
"city": "Budapest",
"street": "Hold u. 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-048444",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "3.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.mnv.hu/karbejelentes, Telefon: +36 80 300 400, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Turisztikai Ügynökség Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@mtu.hu",
"website": "https://www.mtu.hu",
"zip": "1054",
"city": "Budapest",
"street": "Hold u. 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-048555",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.mtu.hu/karbejelentes, Telefon: +36 80 400 500, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Agrár- és Élelmiszeripari Fejlesztési Zrt.",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@agrarbiztosito.hu",
"website": "https://www.agrarbiztosito.hu",
"zip": "1054",
"city": "Budapest",
"street": "Hold u. 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-048666",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "2.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.agrarbiztosito.hu/karbejelentes, Telefon: +36 80 500 600, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Energetikai és Közmű-szabályozási Hivatal (MEKH)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@mekh.hu",
"website": "https://www.mekh.hu",
"zip": "1054",
"city": "Budapest",
"street": "Hold u. 1.",
"raw_data": {
"Cégjegyzékszám": "01-10-048777",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "1.500.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.mekh.hu/karbejelentes, Telefon: +36 80 600 700, Nyitvatartás: H-P 8-16"
}
},
{
"name": "Magyar Nemzeti Bank (MNB Felügyelet)",
"phone": "+36 1 123 4567",
"email": "ugyfelszolgalat@mnb.hu",
"website": "https://www.mnb.hu",
"zip": "1013",
"city": "Budapest",
"street": "Krisztina krt. 6.",
"raw_data": {
"Cégjegyzékszám": "01-10-048888",
"Bankszámla": "117xxxxx-xxxxxxxx-xxxxxxxx",
"Alaptőke": "15.000.000.000 Ft",
"Tulajdonos": "Magyar Állam",
"Kárrendezés": "Kárbejelentés online: www.mnb.hu/karbejelentes, Telefon: +36 80 700 800, Nyitvatartás: H-P 8-16"
}
},
]
# A "Gépjármű biztosító" expertise tag ID-ja
# Ellenőrizve: ID=770, key='gepjarmu_biztosito', level=2, path='penzugy_es_biztositas/gepjarmu_biztosito'
EXPERTISE_TAG_KEY = "gepjarmu_biztosito"
async def seed_insurance_providers():
"""
Feltölti a marketplace.service_providers táblát a magyarországi biztosítókkal.
Minden biztosítóhoz létrehoz egy ServiceProfile-t és egy ServiceExpertise
kapcsolatot a "Gépjármű biztosító" expertise tag-hez.
"""
engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as db:
try:
# 1. Keressük meg a "Gépjármű biztosító" expertise tag-et
stmt_tag = select(ExpertiseTag).where(ExpertiseTag.key == EXPERTISE_TAG_KEY)
result_tag = await db.execute(stmt_tag)
expertise_tag = result_tag.scalar_one_or_none()
if not expertise_tag:
logger.error(f"❌ Expertise tag nem található: {EXPERTISE_TAG_KEY}")
logger.error("Futtasd előbb a seed_expertise_tags.py scriptet!")
return
logger.info(f"✅ Expertise tag megtalálva: {expertise_tag.name_hu} (ID={expertise_tag.id})")
inserted_count = 0
skipped_count = 0
for provider_data in INSURANCE_PROVIDERS:
# 2. Ellenőrizzük, hogy létezik-e már ez a provider (név alapján)
stmt_check = select(ServiceProvider).where(
ServiceProvider.name == provider_data["name"]
)
result_check = await db.execute(stmt_check)
existing_provider = result_check.scalar_one_or_none()
if existing_provider:
logger.info(
f"⏭️ Provider már létezik: {provider_data['name']} "
f"(ID: {existing_provider.id})"
)
skipped_count += 1
continue
# 3. ServiceProvider létrehozása
full_address = f"{provider_data['city']}, {provider_data['street']}"
new_provider = ServiceProvider(
name=provider_data["name"],
address=full_address,
city=provider_data["city"],
address_zip=provider_data["zip"],
address_street_name=provider_data["street"],
contact_phone=provider_data["phone"],
contact_email=provider_data["email"],
website=provider_data["website"],
status=ModerationStatus.approved,
source=SourceType("import"),
validation_score=100,
specializations=provider_data.get("raw_data", {}),
)
db.add(new_provider)
await db.flush()
logger.info(f"✅ ServiceProvider létrehozva: {new_provider.name} (ID={new_provider.id})")
# 4. ServiceProfile létrehozása
fingerprint = hashlib.sha256(
f"{provider_data['name']}:{provider_data['email']}:{provider_data['phone']}".encode()
).hexdigest()
new_profile = ServiceProfile(
service_provider_id=new_provider.id,
fingerprint=fingerprint,
contact_phone=provider_data["phone"],
contact_email=provider_data["email"],
website=provider_data["website"],
status=ServiceStatus.active,
specialization_tags=provider_data.get("raw_data", {}),
)
db.add(new_profile)
await db.flush()
logger.info(f"✅ ServiceProfile létrehozva (ID={new_profile.id})")
# 5. ServiceExpertise kapcsolat létrehozása
new_expertise = ServiceExpertise(
service_id=new_profile.id,
expertise_id=expertise_tag.id,
confidence_level=1.0,
)
db.add(new_expertise)
await db.flush()
logger.info(f"✅ ServiceExpertise kapcsolat létrehozva: {expertise_tag.name_hu}")
inserted_count += 1
await db.commit()
logger.info(
f"\n📊 Összegzés: {inserted_count} új biztosító beszúrva, "
f"{skipped_count} már létezett."
)
except Exception as e:
await db.rollback()
logger.error(f"❌ Hiba a seedelés során: {e}", exc_info=True)
raise
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed_insurance_providers())

View File

@@ -40,7 +40,8 @@ from sqlalchemy.orm import joinedload
from app.models.marketplace.organization import Organization, OrgType, Branch, OrganizationMember, OrgUserRole
from app.models.identity.address import Address, GeoPostalCode
from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise, ServiceStaging
from app.models.marketplace.service import ServiceProfile, ServiceStatus, ExpertiseTag, ServiceExpertise
from app.models.marketplace.staged_data import ServiceStaging
from app.models.identity.social import ServiceProvider, ModerationStatus, SourceType
from app.models.gamification.gamification import PointRule, UserStats
from app.schemas.address import AddressOut
@@ -286,16 +287,17 @@ async def search_providers(
)
org_conditions.append(and_(*token_conditions))
if city:
# P0 UNIFIED ADDRESS REFACTOR (2026-07-01): A denormalizált címmezők
# helyett az Address → GeoPostalCode kapcsolaton keresztül érjük el
# a város és irányítószám adatokat.
# P0 UI/UX OVERHAUL (2026-07-09): Partial fuzzy search on city.
# A város keresés most már ILIKE %{city}% (bárhol a string-ben),
# nem csak StartsWith. Ez lehetővé teszi a részleges városnév
# keresést is (pl. "bud" → "Budapest").
city_clean = city.strip()
org_conditions.append(
or_(
GeoPostalCode.city == city_clean,
GeoPostalCode.city.ilike(f"{city_clean}%"),
GeoPostalCode.city.ilike(f"%{city_clean}%"),
GeoPostalCode.zip_code == city_clean,
GeoPostalCode.zip_code.ilike(f"{city_clean}%"),
GeoPostalCode.zip_code.ilike(f"%{city_clean}%"),
)
)
@@ -374,14 +376,14 @@ async def search_providers(
)
staging_conditions.append(and_(*token_conditions))
if city:
# SZIGORÍTÁS (2026-06-17): City keresés exact match vagy StartsWith
# P0 UI/UX OVERHAUL (2026-07-09): Partial fuzzy search on city
city_clean = city.strip()
staging_conditions.append(
or_(
ServiceStaging.city == city_clean,
ServiceStaging.city.ilike(f"{city_clean}%"),
ServiceStaging.city.ilike(f"%{city_clean}%"),
ServiceStaging.postal_code == city_clean,
ServiceStaging.postal_code.ilike(f"{city_clean}%"),
ServiceStaging.postal_code.ilike(f"%{city_clean}%"),
)
)
@@ -415,12 +417,12 @@ async def search_providers(
)
crowd_conditions.append(and_(*token_conditions))
if city:
# SZIGORÍTÁS (2026-06-17): City keresés exact match vagy StartsWith
# P0 UI/UX OVERHAUL (2026-07-09): Partial fuzzy search on city
city_clean = city.strip()
crowd_conditions.append(
or_(
ServiceProvider.address == city_clean,
ServiceProvider.address.ilike(f"{city_clean}%"),
ServiceProvider.address.ilike(f"%{city_clean}%"),
)
)
@@ -532,16 +534,26 @@ async def search_providers(
row_city = row.city
row_plus_code = getattr(row, "plus_code", None)
# P0 BUGFIX (2026-07-10): Null-safe AddressOut construction.
# A Pydantic v2 from_attributes=True mode-ban néha hibát dob,
# ha minden mező None. Itt try-excepttel védjük.
address_detail = None
if any([row_address_zip, row_address_street_name, row_address_street_type, row_address_house_number, row_city]):
address_detail = AddressOut(
zip=row_address_zip,
city=row_city,
street_name=row_address_street_name,
street_type=row_address_street_type,
house_number=row_address_house_number,
full_address_text=row.address,
)
try:
address_detail = AddressOut(
zip=row_address_zip,
city=row_city,
street_name=row_address_street_name,
street_type=row_address_street_type,
house_number=row_address_house_number,
full_address_text=row.address,
)
except Exception as addr_e:
logger.warning(
f"Failed to create AddressOut for provider {row.id} ({row.name}): {addr_e}. "
f"Skipping address_detail."
)
address_detail = None
results.append(
ProviderSearchResult(

View File

@@ -0,0 +1,53 @@
import asyncio, uuid
from app.database import AsyncSessionLocal
from app.models.identity.identity import User, UserRole, Wallet
from app.models.system.audit import SecurityAuditLog, FinancialLedger
from sqlalchemy import select, text
async def test():
uid = str(uuid.uuid4())[:8]
email = f'hard_delete_test_{uid}@example.com'
async with AsyncSessionLocal() as db:
# Insert test user
result = await db.execute(
text("""
INSERT INTO identity.users
(email, hashed_password, role, subscription_plan, preferred_language, region_code, preferred_currency, ui_mode, is_vip, is_active, is_deleted, created_at, scope_level, custom_permissions, alternative_emails, email_history, visual_settings)
VALUES
(:email, 'fakehash', 'USER', 'FREE', 'en', 'HU', 'HUF', 'personal', false, false, false, NOW(), 'individual', '{}'::jsonb, '[]'::jsonb, '[]'::jsonb, '{"theme": "default", "primary_color": null, "wall_logo_url": null}'::jsonb)
RETURNING id
"""),
{"email": email}
)
await db.commit()
result = await db.execute(select(User).where(User.email == email))
u = result.scalar_one_or_none()
test_id = u.id
print(f'Created test user #{test_id}: {u.email}')
# Verify no wallet/ledger
wr = await db.execute(select(Wallet).where(Wallet.user_id == test_id).limit(1))
lr = await db.execute(select(FinancialLedger).where(FinancialLedger.user_id == test_id).limit(1))
print(f' Has Wallet: {wr.scalar_one_or_none() is not None}')
print(f' Has Ledger: {lr.scalar_one_or_none() is not None}')
# Simulate the FIXED hard delete: delete first, then audit
await db.delete(u)
await db.flush()
audit = SecurityAuditLog(
actor_id=1, target_id=test_id, action='hard_delete_user',
is_critical=True,
payload_before={'user_id': test_id, 'email': email, 'role': 'USER'},
payload_after={'details': f'User #{test_id} permanently deleted by admin #1'},
)
db.add(audit)
await db.commit()
check = await db.get(User, test_id)
print(f' User after delete: {check}')
print('✅ Hard delete simulation SUCCESSFUL')
asyncio.run(test())