garázs fejlesztés
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
320
backend/app/scripts/seed_expertise_enterprise.py
Normal file
320
backend/app/scripts/seed_expertise_enterprise.py
Normal 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()
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
695
backend/app/scripts/seed_insurance_providers.py
Normal file
695
backend/app/scripts/seed_insurance_providers.py
Normal 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())
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user