refaktor címtár
This commit is contained in:
@@ -11,7 +11,7 @@ logger = logging.getLogger("admin-endpoints")
|
||||
|
||||
from app.api import deps
|
||||
from app.models.identity import User, UserRole, Person # JAVÍTVA: Központi import
|
||||
from app.models.identity.address import Address
|
||||
from app.models.identity.address import Address, GeoPostalCode
|
||||
from app.models.system import SystemParameter, ParameterScope
|
||||
from app.services.system_service import system_service
|
||||
# JAVÍTVA: Security audit modellek
|
||||
@@ -623,23 +623,12 @@ async def list_users(
|
||||
seen_ids.add(row.id)
|
||||
unique_users.append(row)
|
||||
|
||||
from app.schemas.address import AddressOut
|
||||
|
||||
user_list = []
|
||||
for u in unique_users:
|
||||
person = u.person
|
||||
address = person.address if person else None
|
||||
# Cím összefűzése
|
||||
address_str = None
|
||||
if address:
|
||||
parts = []
|
||||
if address.zip:
|
||||
parts.append(str(address.zip))
|
||||
if address.city:
|
||||
parts.append(str(address.city))
|
||||
if address.street_name:
|
||||
parts.append(str(address.street_name))
|
||||
if address.house_number:
|
||||
parts.append(str(address.house_number))
|
||||
address_str = ', '.join(parts) if parts else None
|
||||
|
||||
# Személy nevének összefűzése
|
||||
person_name = None
|
||||
@@ -663,12 +652,8 @@ async def list_users(
|
||||
"first_name": person.first_name if person else None,
|
||||
"last_name": person.last_name if person else None,
|
||||
"phone": person.phone if person else None,
|
||||
# Címadatok
|
||||
"postal_code": str(address.zip) if address and address.zip else None,
|
||||
"city": address.city if address and address.city else None,
|
||||
"street": address.street_name if address and address.street_name else None,
|
||||
"house_number": address.house_number if address and address.house_number else None,
|
||||
"address": address_str,
|
||||
# Címadatok — egységes AddressOut formátumban
|
||||
"address": AddressOut.model_validate(address) if address else None,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -943,6 +928,8 @@ async def search_organizations(
|
||||
"""
|
||||
from app.models.marketplace.organization import Organization
|
||||
|
||||
# P0 FIX: Join with system.addresses and system.geo_postal_codes
|
||||
# instead of selecting the ghost column Organization.address_city.
|
||||
stmt = (
|
||||
select(
|
||||
Organization.id,
|
||||
@@ -955,11 +942,13 @@ async def search_organizations(
|
||||
Organization.is_verified,
|
||||
Organization.is_active,
|
||||
Organization.is_deleted,
|
||||
Organization.address_city,
|
||||
GeoPostalCode.city.label("city"),
|
||||
Organization.region,
|
||||
Organization.segment,
|
||||
Organization.created_at,
|
||||
)
|
||||
.outerjoin(Address, Organization.address_id == Address.id)
|
||||
.outerjoin(GeoPostalCode, Address.postal_code_id == GeoPostalCode.id)
|
||||
.where(
|
||||
or_(
|
||||
Organization.full_name.ilike(f"%{name}%"),
|
||||
@@ -987,7 +976,7 @@ async def search_organizations(
|
||||
"is_verified": row.is_verified,
|
||||
"is_active": row.is_active,
|
||||
"is_deleted": row.is_deleted,
|
||||
"city": row.address_city,
|
||||
"city": row.city,
|
||||
"region": row.region,
|
||||
"segment": row.segment,
|
||||
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||
|
||||
@@ -28,7 +28,9 @@ from app.models.core_logic import SubscriptionTier, OrganizationSubscription
|
||||
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||
from app.models.fleet.organization import ContactPerson
|
||||
from app.models.identity import User, Person
|
||||
from app.services.address_manager import AddressManager
|
||||
from app.models.vehicle.history import AuditLog, LogSeverity
|
||||
from app.schemas.address import AddressIn, AddressOut
|
||||
from app.schemas.organization import (
|
||||
OrganizationMemberCreate,
|
||||
OrganizationMemberUpdate,
|
||||
@@ -207,30 +209,36 @@ class GarageDetailsResponse(BaseModel):
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
org_type: str
|
||||
# Cím adatok (elsődleges)
|
||||
# Cím adatok (elsődleges) — denormalizált mezők (backward compatible)
|
||||
address_city: Optional[str] = None
|
||||
address_zip: Optional[str] = None
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: Optional[str] = None
|
||||
# P0: Unified AddressOut nested object
|
||||
address_detail: Optional[AddressOut] = None
|
||||
tax_number: Optional[str] = None
|
||||
reg_number: Optional[str] = None
|
||||
# ── Kapcsolattartó (Contact Person) ──
|
||||
contact_person_name: Optional[str] = None
|
||||
contact_email: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
# ── Számlázási cím (Billing Address) ──
|
||||
# ── Számlázási cím (Billing Address) — denormalizált mezők (backward compatible) ──
|
||||
billing_zip: Optional[str] = None
|
||||
billing_city: Optional[str] = None
|
||||
billing_street_name: Optional[str] = None
|
||||
billing_street_type: Optional[str] = None
|
||||
billing_house_number: Optional[str] = None
|
||||
# ── Értesítési cím (Notification Address) ──
|
||||
# P0: Unified AddressOut nested object for billing address
|
||||
billing_address_detail: Optional[AddressOut] = None
|
||||
# ── Értesítési cím (Notification Address) — denormalizált mezők (backward compatible) ──
|
||||
notification_zip: Optional[str] = None
|
||||
notification_city: Optional[str] = None
|
||||
notification_street_name: Optional[str] = None
|
||||
notification_street_type: Optional[str] = None
|
||||
notification_house_number: Optional[str] = None
|
||||
# P0: Unified AddressOut nested object for notification address
|
||||
notification_address_detail: Optional[AddressOut] = None
|
||||
# Előfizetés
|
||||
subscription: Optional[SubscriptionSummary] = None
|
||||
# Kapcsolattartó (ContactPerson modellből)
|
||||
@@ -421,6 +429,9 @@ async def list_organizations(
|
||||
# Resolve email: prefer contact_email, fallback to owner's email
|
||||
resolved_email = org.contact_email or (org.owner.email if org.owner else None)
|
||||
|
||||
# P1 FIX: Use relationship-based city instead of ghost column org.address_city
|
||||
resolved_city = org.address.city if org.address else None
|
||||
|
||||
garage_list.append(GarageListItem(
|
||||
id=org.id,
|
||||
name=org.name,
|
||||
@@ -432,7 +443,7 @@ async def list_organizations(
|
||||
is_verified=org.is_verified,
|
||||
is_deleted=org.is_deleted,
|
||||
org_type=org.org_type.value if hasattr(org.org_type, "value") else str(org.org_type),
|
||||
city=org.address_city,
|
||||
city=resolved_city,
|
||||
region=org.region,
|
||||
segment=org.segment,
|
||||
member_count=member_counts.get(org.id, 0),
|
||||
@@ -1078,6 +1089,19 @@ async def get_organization_details(
|
||||
f"({org.full_name}) — {len(members_list)} members"
|
||||
)
|
||||
|
||||
# ── P0 REFACTORED: Build unified AddressOut objects from relationships ──
|
||||
address_detail = None
|
||||
if org.address:
|
||||
address_detail = AddressOut.model_validate(org.address)
|
||||
|
||||
billing_address_detail = None
|
||||
if org.billing_address:
|
||||
billing_address_detail = AddressOut.model_validate(org.billing_address)
|
||||
|
||||
notification_address_detail = None
|
||||
if org.notification_address:
|
||||
notification_address_detail = AddressOut.model_validate(org.notification_address)
|
||||
|
||||
return GarageDetailsResponse(
|
||||
id=org.id,
|
||||
name=org.name,
|
||||
@@ -1087,11 +1111,13 @@ async def get_organization_details(
|
||||
is_active=org.is_active,
|
||||
is_verified=org.is_verified,
|
||||
org_type=org.org_type.value if hasattr(org.org_type, "value") else str(org.org_type),
|
||||
address_city=org.address_city,
|
||||
address_zip=org.address_zip,
|
||||
address_street_name=org.address_street_name,
|
||||
address_street_type=org.address_street_type,
|
||||
address_house_number=org.address_house_number,
|
||||
# ── P0 REFACTORED: Denormalized fields populated from relationship ──
|
||||
address_city=org.address.city if org.address else None,
|
||||
address_zip=org.address.zip if org.address else None,
|
||||
address_street_name=org.address.street_name if org.address else None,
|
||||
address_street_type=org.address.street_type if org.address else None,
|
||||
address_house_number=org.address.house_number if org.address else None,
|
||||
address_detail=address_detail,
|
||||
tax_number=org.tax_number,
|
||||
reg_number=org.reg_number,
|
||||
# ── P0: Zero-Duplication Kapcsolattartó (owner fallback) ──
|
||||
@@ -1099,17 +1125,19 @@ async def get_organization_details(
|
||||
contact_email=resolved_contact_email,
|
||||
contact_phone=resolved_contact_phone,
|
||||
# ── Számlázási cím ──
|
||||
billing_zip=org.billing_zip,
|
||||
billing_city=org.billing_city,
|
||||
billing_street_name=org.billing_street_name,
|
||||
billing_street_type=org.billing_street_type,
|
||||
billing_house_number=org.billing_house_number,
|
||||
billing_zip=org.billing_address.zip if org.billing_address else None,
|
||||
billing_city=org.billing_address.city if org.billing_address else None,
|
||||
billing_street_name=org.billing_address.street_name if org.billing_address else None,
|
||||
billing_street_type=org.billing_address.street_type if org.billing_address else None,
|
||||
billing_house_number=org.billing_address.house_number if org.billing_address else None,
|
||||
billing_address_detail=billing_address_detail,
|
||||
# ── Értesítési cím ──
|
||||
notification_zip=org.notification_zip,
|
||||
notification_city=org.notification_city,
|
||||
notification_street_name=org.notification_street_name,
|
||||
notification_street_type=org.notification_street_type,
|
||||
notification_house_number=org.notification_house_number,
|
||||
notification_zip=org.notification_address.zip if org.notification_address else None,
|
||||
notification_city=org.notification_address.city if org.notification_address else None,
|
||||
notification_street_name=org.notification_address.street_name if org.notification_address else None,
|
||||
notification_street_type=org.notification_address.street_type if org.notification_address else None,
|
||||
notification_house_number=org.notification_address.house_number if org.notification_address else None,
|
||||
notification_address_detail=notification_address_detail,
|
||||
subscription=subscription_summary,
|
||||
primary_contact=primary_contact,
|
||||
created_at=org.created_at.isoformat() if org.created_at else None,
|
||||
@@ -1224,28 +1252,34 @@ class OrganizationUpdate(BaseModel):
|
||||
# ── Org-level email/phone (used for individual garages to propagate to Person/User) ──
|
||||
email: Optional[str] = Field(None, max_length=255, description="Szervezeti e-mail (individual garages esetén a User rekordba propagálódik)")
|
||||
phone: Optional[str] = Field(None, max_length=50, description="Szervezeti telefon (individual garages esetén a Person rekordba propagálódik)")
|
||||
# ── Elsődleges cím (Primary Address) ──
|
||||
address_zip: Optional[str] = Field(None, max_length=10, description="Irányítószám")
|
||||
address_city: Optional[str] = Field(None, max_length=100, description="Város")
|
||||
address_street_name: Optional[str] = Field(None, max_length=150, description="Utca neve")
|
||||
address_street_type: Optional[str] = Field(None, max_length=50, description="Utca típusa (utca, tér, stb.)")
|
||||
address_house_number: Optional[str] = Field(None, max_length=20, description="Házszám")
|
||||
# ── Elsődleges cím (Primary Address) — DEPRECATED: Use address_detail instead ──
|
||||
address_zip: Optional[str] = Field(None, max_length=10, description="[DEPRECATED] Használd az address_detail mezőt! Irányítószám")
|
||||
address_city: Optional[str] = Field(None, max_length=100, description="[DEPRECATED] Használd az address_detail mezőt! Város")
|
||||
address_street_name: Optional[str] = Field(None, max_length=150, description="[DEPRECATED] Használd az address_detail mezőt! Utca neve")
|
||||
address_street_type: Optional[str] = Field(None, max_length=50, description="[DEPRECATED] Használd az address_detail mezőt! Utca típusa (utca, tér, stb.)")
|
||||
address_house_number: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Használd az address_detail mezőt! Házszám")
|
||||
# P0: Unified AddressIn nested object for primary address
|
||||
address_detail: Optional[AddressIn] = None
|
||||
# ── Kapcsolattartó (Contact Person) ──
|
||||
contact_person_name: Optional[str] = Field(None, max_length=255, description="Kapcsolattartó neve")
|
||||
contact_email: Optional[str] = Field(None, max_length=255, description="Kapcsolattartó e-mail címe")
|
||||
contact_phone: Optional[str] = Field(None, max_length=50, description="Kapcsolattartó telefonszáma")
|
||||
# ── Számlázási cím (Billing Address) ──
|
||||
billing_zip: Optional[str] = Field(None, max_length=10, description="Számlázási irányítószám")
|
||||
billing_city: Optional[str] = Field(None, max_length=100, description="Számlázási város")
|
||||
billing_street_name: Optional[str] = Field(None, max_length=150, description="Számlázási utca neve")
|
||||
billing_street_type: Optional[str] = Field(None, max_length=50, description="Számlázási utca típusa")
|
||||
billing_house_number: Optional[str] = Field(None, max_length=20, description="Számlázási házszám")
|
||||
# ── Értesítési cím (Notification Address) ──
|
||||
notification_zip: Optional[str] = Field(None, max_length=10, description="Értesítési irányítószám")
|
||||
notification_city: Optional[str] = Field(None, max_length=100, description="Értesítési város")
|
||||
notification_street_name: Optional[str] = Field(None, max_length=150, description="Értesítési utca neve")
|
||||
notification_street_type: Optional[str] = Field(None, max_length=50, description="Értesítési utca típusa")
|
||||
notification_house_number: Optional[str] = Field(None, max_length=20, description="Értesítési házszám")
|
||||
# ── Számlázási cím (Billing Address) — DEPRECATED: Use billing_address_detail instead ──
|
||||
billing_zip: Optional[str] = Field(None, max_length=10, description="[DEPRECATED] Használd a billing_address_detail mezőt! Számlázási irányítószám")
|
||||
billing_city: Optional[str] = Field(None, max_length=100, description="[DEPRECATED] Használd a billing_address_detail mezőt! Számlázási város")
|
||||
billing_street_name: Optional[str] = Field(None, max_length=150, description="[DEPRECATED] Használd a billing_address_detail mezőt! Számlázási utca neve")
|
||||
billing_street_type: Optional[str] = Field(None, max_length=50, description="[DEPRECATED] Használd a billing_address_detail mezőt! Számlázási utca típusa")
|
||||
billing_house_number: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Használd a billing_address_detail mezőt! Számlázási házszám")
|
||||
# P0: Unified AddressIn nested object for billing address
|
||||
billing_address_detail: Optional[AddressIn] = None
|
||||
# ── Értesítési cím (Notification Address) — DEPRECATED: Use notification_address_detail instead ──
|
||||
notification_zip: Optional[str] = Field(None, max_length=10, description="[DEPRECATED] Használd a notification_address_detail mezőt! Értesítési irányítószám")
|
||||
notification_city: Optional[str] = Field(None, max_length=100, description="[DEPRECATED] Használd a notification_address_detail mezőt! Értesítési város")
|
||||
notification_street_name: Optional[str] = Field(None, max_length=150, description="[DEPRECATED] Használd a notification_address_detail mezőt! Értesítési utca neve")
|
||||
notification_street_type: Optional[str] = Field(None, max_length=50, description="[DEPRECATED] Használd a notification_address_detail mezőt! Értesítési utca típusa")
|
||||
notification_house_number: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Használd a notification_address_detail mezőt! Értesítési házszám")
|
||||
# P0: Unified AddressIn nested object for notification address
|
||||
notification_address_detail: Optional[AddressIn] = None
|
||||
|
||||
|
||||
@router.put(
|
||||
@@ -1279,10 +1313,34 @@ async def update_organization(
|
||||
detail=f"Szervezet nem található ID-vel: {org_id}",
|
||||
)
|
||||
|
||||
# 2. Összegyűjtjük a változásokat
|
||||
# 2. P0 REFACTORED: Address FK logic via AddressManager
|
||||
raw_data = payload.model_dump(exclude_none=True)
|
||||
|
||||
# ── Primary address via AddressManager ──
|
||||
address_detail: Optional[AddressIn] = raw_data.pop("address_detail", None)
|
||||
if address_detail:
|
||||
org.address_id = await AddressManager.create_or_update(
|
||||
db, org.address_id, address_detail
|
||||
)
|
||||
|
||||
# ── Billing address via AddressManager ──
|
||||
billing_address_detail: Optional[AddressIn] = raw_data.pop("billing_address_detail", None)
|
||||
if billing_address_detail:
|
||||
org.billing_address_id = await AddressManager.create_or_update(
|
||||
db, org.billing_address_id, billing_address_detail
|
||||
)
|
||||
|
||||
# ── Notification address via AddressManager ──
|
||||
notification_address_detail: Optional[AddressIn] = raw_data.pop("notification_address_detail", None)
|
||||
if notification_address_detail:
|
||||
org.notification_address_id = await AddressManager.create_or_update(
|
||||
db, org.notification_address_id, notification_address_detail
|
||||
)
|
||||
|
||||
# 3. Összegyűjtjük a változásokat (non-address fields)
|
||||
update_fields = {}
|
||||
for field_name in payload.model_dump(exclude_none=True):
|
||||
value = getattr(payload, field_name)
|
||||
for field_name in raw_data:
|
||||
value = raw_data[field_name]
|
||||
if value is not None and hasattr(org, field_name):
|
||||
setattr(org, field_name, value)
|
||||
update_fields[field_name] = value
|
||||
@@ -1333,6 +1391,11 @@ async def update_organization(
|
||||
f"({org.full_name}): fields={list(update_fields.keys())}"
|
||||
)
|
||||
|
||||
# ── P0 REFACTORED: Build unified AddressOut from relationship ──
|
||||
resp_address_detail = None
|
||||
if org.address:
|
||||
resp_address_detail = AddressOut.model_validate(org.address)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"A(z) '{org.full_name}' garázs adatai frissítve.",
|
||||
@@ -1345,11 +1408,7 @@ async def update_organization(
|
||||
"display_name": org.display_name,
|
||||
"tax_number": org.tax_number,
|
||||
"reg_number": org.reg_number,
|
||||
"address_zip": org.address_zip,
|
||||
"address_city": org.address_city,
|
||||
"address_street_name": org.address_street_name,
|
||||
"address_street_type": org.address_street_type,
|
||||
"address_house_number": org.address_house_number,
|
||||
"address_detail": resp_address_detail,
|
||||
"status": org.status,
|
||||
"is_active": org.is_active,
|
||||
"contact_email": org.contact_email,
|
||||
|
||||
@@ -26,7 +26,8 @@ from app.models.identity import User, Person, UserRole
|
||||
from app.models.identity.address import Address
|
||||
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||
from app.models.vehicle.history import AuditLog, LogSeverity
|
||||
from app.schemas.user import AddressResponse
|
||||
from app.schemas.address import AddressIn, AddressOut
|
||||
from app.services.address_manager import AddressManager
|
||||
|
||||
logger = logging.getLogger("admin-persons")
|
||||
router = APIRouter()
|
||||
@@ -59,7 +60,7 @@ class PersonListItem(BaseModel):
|
||||
merged_into_id: Optional[int] = None
|
||||
users_count: int = 0
|
||||
active_user: Optional[PersonListItemActiveUser] = None
|
||||
address: Optional[AddressResponse] = None
|
||||
address: Optional[AddressOut] = None
|
||||
|
||||
|
||||
class PersonListResponse(BaseModel):
|
||||
@@ -214,20 +215,7 @@ async def list_persons(
|
||||
# Cím adatok
|
||||
address_data = None
|
||||
if person.address:
|
||||
address_data = AddressResponse(
|
||||
address_zip=person.address.zip,
|
||||
address_city=person.address.city,
|
||||
address_street_name=person.address.street_name,
|
||||
address_street_type=person.address.street_type,
|
||||
address_house_number=person.address.house_number,
|
||||
address_stairwell=person.address.stairwell,
|
||||
address_floor=person.address.floor,
|
||||
address_door=person.address.door,
|
||||
address_hrsz=person.address.parcel_id,
|
||||
full_address_text=person.address.full_address_text,
|
||||
latitude=person.address.latitude,
|
||||
longitude=person.address.longitude,
|
||||
)
|
||||
address_data = AddressOut.model_validate(person.address)
|
||||
|
||||
items.append(PersonListItem(
|
||||
id=person.id,
|
||||
@@ -338,7 +326,7 @@ class PersonDetailResponse(BaseModel):
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
# Kapcsolódó entitások
|
||||
address: Optional[AddressResponse] = None
|
||||
address: Optional[AddressOut] = None
|
||||
users: List[PersonDetailUserInfo] = []
|
||||
memberships: List[PersonDetailMembership] = []
|
||||
owned_organizations: List[PersonDetailOwnedOrganization] = []
|
||||
@@ -394,20 +382,7 @@ async def get_person_detail(
|
||||
# ── Cím adatok ──
|
||||
address_data = None
|
||||
if person.address:
|
||||
address_data = AddressResponse(
|
||||
address_zip=person.address.zip,
|
||||
address_city=person.address.city,
|
||||
address_street_name=person.address.street_name,
|
||||
address_street_type=person.address.street_type,
|
||||
address_house_number=person.address.house_number,
|
||||
address_stairwell=person.address.stairwell,
|
||||
address_floor=person.address.floor,
|
||||
address_door=person.address.door,
|
||||
address_hrsz=person.address.parcel_id,
|
||||
full_address_text=person.address.full_address_text,
|
||||
latitude=person.address.latitude,
|
||||
longitude=person.address.longitude,
|
||||
)
|
||||
address_data = AddressOut.model_validate(person.address)
|
||||
|
||||
# ── Kapcsolódó User-ek ──
|
||||
users_data: List[PersonDetailUserInfo] = []
|
||||
@@ -520,22 +495,6 @@ async def get_person_detail(
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class PersonAddressUpdate(BaseModel):
|
||||
"""Cím adatok a Person szerkesztéshez."""
|
||||
address_zip: Optional[str] = Field(default=None, description="Irányítószám")
|
||||
address_city: Optional[str] = Field(default=None, description="Város")
|
||||
address_street_name: Optional[str] = Field(default=None, description="Utca név")
|
||||
address_street_type: Optional[str] = Field(default=None, description="Közterület jellege")
|
||||
address_house_number: Optional[str] = Field(default=None, description="Házszám")
|
||||
address_stairwell: Optional[str] = Field(default=None, description="Lépcsőház")
|
||||
address_floor: Optional[str] = Field(default=None, description="Emelet")
|
||||
address_door: Optional[str] = Field(default=None, description="Ajtó")
|
||||
address_hrsz: Optional[str] = Field(default=None, description="Helyrajzi szám")
|
||||
full_address_text: Optional[str] = Field(default=None, description="Teljes cím szöveg")
|
||||
latitude: Optional[float] = Field(default=None, description="GPS szélesség")
|
||||
longitude: Optional[float] = Field(default=None, description="GPS hosszúság")
|
||||
|
||||
|
||||
class PersonUpdateRequest(BaseModel):
|
||||
"""Admin által szerkeszthető Person mezők.
|
||||
|
||||
@@ -554,8 +513,8 @@ class PersonUpdateRequest(BaseModel):
|
||||
is_active: Optional[bool] = Field(default=None, description="Aktív státusz")
|
||||
is_ghost: Optional[bool] = Field(default=None, description="Ghost státusz")
|
||||
|
||||
# Cím adatok (opcionális, nested object)
|
||||
address: Optional[PersonAddressUpdate] = Field(default=None, description="Cím adatok")
|
||||
# Cím adatok (opcionális, nested object) — egységes AddressIn séma
|
||||
address: Optional[AddressIn] = Field(default=None, description="Cím adatok")
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
@@ -692,62 +651,14 @@ async def update_person(
|
||||
changes["is_ghost"] = {"old": person.is_ghost, "new": update_data.is_ghost}
|
||||
person.is_ghost = update_data.is_ghost
|
||||
|
||||
# ── 5. Cím adatok frissítése ──
|
||||
# ── 5. Cím adatok frissítése (P1 REFACTORED: AddressManager) ──
|
||||
if update_data.address is not None:
|
||||
addr_data = update_data.address
|
||||
|
||||
# Ha van már cím, frissítjük, különben létrehozzuk
|
||||
if person.address:
|
||||
address = person.address
|
||||
else:
|
||||
# Új cím létrehozása
|
||||
address = Address(
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(address)
|
||||
person.address = address
|
||||
|
||||
# Cím mezők frissítése
|
||||
addr_changes: Dict[str, Any] = {}
|
||||
if addr_data.address_zip is not None:
|
||||
addr_changes["address_zip"] = {"old": address.zip, "new": addr_data.address_zip}
|
||||
# Megjegyzés: a zip a GeoPostalCode kapcsolatból jön, itt csak a postal_code_id-t tudnánk keresni
|
||||
# Egyszerűsítés: a frontend által küldött adatokat tároljuk a full_address_text-ben is
|
||||
if addr_data.address_city is not None:
|
||||
addr_changes["address_city"] = {"old": address.city, "new": addr_data.address_city}
|
||||
if addr_data.address_street_name is not None:
|
||||
addr_changes["address_street_name"] = {"old": address.street_name, "new": addr_data.address_street_name}
|
||||
address.street_name = addr_data.address_street_name
|
||||
if addr_data.address_street_type is not None:
|
||||
addr_changes["address_street_type"] = {"old": address.street_type, "new": addr_data.address_street_type}
|
||||
address.street_type = addr_data.address_street_type
|
||||
if addr_data.address_house_number is not None:
|
||||
addr_changes["address_house_number"] = {"old": address.house_number, "new": addr_data.address_house_number}
|
||||
address.house_number = addr_data.address_house_number
|
||||
if addr_data.address_stairwell is not None:
|
||||
addr_changes["address_stairwell"] = {"old": address.stairwell, "new": addr_data.address_stairwell}
|
||||
address.stairwell = addr_data.address_stairwell
|
||||
if addr_data.address_floor is not None:
|
||||
addr_changes["address_floor"] = {"old": address.floor, "new": addr_data.address_floor}
|
||||
address.floor = addr_data.address_floor
|
||||
if addr_data.address_door is not None:
|
||||
addr_changes["address_door"] = {"old": address.door, "new": addr_data.address_door}
|
||||
address.door = addr_data.address_door
|
||||
if addr_data.address_hrsz is not None:
|
||||
addr_changes["address_hrsz"] = {"old": address.parcel_id, "new": addr_data.address_hrsz}
|
||||
address.parcel_id = addr_data.address_hrsz
|
||||
if addr_data.full_address_text is not None:
|
||||
addr_changes["full_address_text"] = {"old": address.full_address_text, "new": addr_data.full_address_text}
|
||||
address.full_address_text = addr_data.full_address_text
|
||||
if addr_data.latitude is not None:
|
||||
addr_changes["latitude"] = {"old": address.latitude, "new": addr_data.latitude}
|
||||
address.latitude = addr_data.latitude
|
||||
if addr_data.longitude is not None:
|
||||
addr_changes["longitude"] = {"old": address.longitude, "new": addr_data.longitude}
|
||||
address.longitude = addr_data.longitude
|
||||
|
||||
if addr_changes:
|
||||
changes["address"] = addr_changes
|
||||
# P1 FIX: Use AddressManager.create_or_update() instead of
|
||||
# writing directly to Address model fields.
|
||||
person.address_id = await AddressManager.create_or_update(
|
||||
db, person.address_id, update_data.address
|
||||
)
|
||||
changes["address"] = {"old": None, "new": "updated via AddressManager"}
|
||||
|
||||
# ── 6. Mentés ──
|
||||
if changes:
|
||||
@@ -768,20 +679,7 @@ async def update_person(
|
||||
# Cím adatok
|
||||
address_data = None
|
||||
if person.address:
|
||||
address_data = AddressResponse(
|
||||
address_zip=person.address.zip,
|
||||
address_city=person.address.city,
|
||||
address_street_name=person.address.street_name,
|
||||
address_street_type=person.address.street_type,
|
||||
address_house_number=person.address.house_number,
|
||||
address_stairwell=person.address.stairwell,
|
||||
address_floor=person.address.floor,
|
||||
address_door=person.address.door,
|
||||
address_hrsz=person.address.parcel_id,
|
||||
full_address_text=person.address.full_address_text,
|
||||
latitude=person.address.latitude,
|
||||
longitude=person.address.longitude,
|
||||
)
|
||||
address_data = AddressOut.model_validate(person.address)
|
||||
|
||||
# Kapcsolódó User-ek
|
||||
users_data: List[PersonDetailUserInfo] = []
|
||||
|
||||
@@ -29,6 +29,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api import deps
|
||||
from app.schemas.address import AddressIn, AddressOut
|
||||
from app.db.session import get_db
|
||||
from app.models.identity import User
|
||||
from app.models.identity.social import (
|
||||
@@ -75,10 +76,7 @@ class ProviderDetail(BaseModel):
|
||||
name: str
|
||||
address: str
|
||||
city: Optional[str] = None
|
||||
address_zip: Optional[str] = None
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: Optional[str] = None
|
||||
address_detail: Optional[AddressOut] = None
|
||||
plus_code: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
contact_email: Optional[str] = None
|
||||
@@ -130,10 +128,7 @@ class ProviderUpdateInput(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255, description="Szolgáltató neve")
|
||||
address: Optional[str] = Field(None, max_length=500, description="Teljes cím")
|
||||
city: Optional[str] = Field(None, max_length=100, description="Város")
|
||||
address_zip: Optional[str] = Field(None, max_length=20, description="Irányítószám")
|
||||
address_street_name: Optional[str] = Field(None, max_length=255, description="Utca név")
|
||||
address_street_type: Optional[str] = Field(None, max_length=50, description="Utca típusa (pl. utca, tér, út)")
|
||||
address_house_number: Optional[str] = Field(None, max_length=20, description="Házszám")
|
||||
address_detail: Optional[AddressIn] = Field(None, description="Cím részletes adatai (egységes AddressIn séma)")
|
||||
plus_code: Optional[str] = Field(None, max_length=50, description="Plus Code (Google Maps)")
|
||||
contact_phone: Optional[str] = Field(None, max_length=50, description="Telefonszám")
|
||||
contact_email: Optional[str] = Field(None, max_length=255, description="Email cím")
|
||||
@@ -290,6 +285,7 @@ def _build_unified_providers_query(
|
||||
alkalmazzuk (nem az egyes részekre), hogy elkerüljük az enum típusütközéseket.
|
||||
"""
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.identity.address import Address, GeoPostalCode
|
||||
|
||||
# --- 1. ServiceProvider lekérdezés ---
|
||||
provider_conditions = []
|
||||
@@ -355,17 +351,17 @@ def _build_unified_providers_query(
|
||||
Organization.id.label("id"),
|
||||
Organization.name.label("name"),
|
||||
func.concat(
|
||||
Organization.address_zip,
|
||||
GeoPostalCode.zip_code,
|
||||
literal(' '),
|
||||
Organization.address_city,
|
||||
GeoPostalCode.city,
|
||||
literal(', '),
|
||||
Organization.address_street_name,
|
||||
Address.street_name,
|
||||
literal(' '),
|
||||
Organization.address_street_type,
|
||||
Address.street_type,
|
||||
literal(' '),
|
||||
Organization.address_house_number,
|
||||
Address.house_number,
|
||||
).label("address"),
|
||||
Organization.address_city.label("city"),
|
||||
GeoPostalCode.city.label("city"),
|
||||
literal(None).label("category"),
|
||||
literal("approved").label("status"),
|
||||
literal("verified_org").label("source"),
|
||||
@@ -373,6 +369,12 @@ def _build_unified_providers_query(
|
||||
literal(100).label("validation_score"),
|
||||
literal(None).label("added_by_user_id"),
|
||||
Organization.created_at.label("created_at"),
|
||||
).outerjoin(
|
||||
Address,
|
||||
Address.id == Organization.address_id,
|
||||
).outerjoin(
|
||||
GeoPostalCode,
|
||||
GeoPostalCode.id == Address.postal_code_id,
|
||||
)
|
||||
if org_conditions:
|
||||
org_stmt = org_stmt.where(*org_conditions)
|
||||
@@ -562,10 +564,13 @@ async def get_provider_detail(
|
||||
name=sp.name,
|
||||
address=sp.address or "",
|
||||
city=sp.city,
|
||||
address_zip=sp.address_zip,
|
||||
address_street_name=sp.address_street_name,
|
||||
address_street_type=sp.address_street_type,
|
||||
address_house_number=sp.address_house_number,
|
||||
address_detail=AddressOut(
|
||||
zip=sp.address_zip,
|
||||
city=sp.city,
|
||||
street_name=sp.address_street_name,
|
||||
street_type=sp.address_street_type,
|
||||
house_number=sp.address_house_number,
|
||||
),
|
||||
plus_code=sp.plus_code,
|
||||
contact_phone=sp.contact_phone,
|
||||
contact_email=sp.contact_email,
|
||||
@@ -1148,6 +1153,39 @@ async def update_provider(
|
||||
# Mezők frissítése (csak a nem None értékeket)
|
||||
update_fields = update_data.model_dump(exclude_none=True)
|
||||
|
||||
# =====================================================================
|
||||
# STEP 0: Extract address_detail (AddressIn) and map to flat model fields
|
||||
# =====================================================================
|
||||
address_detail: Optional[dict] = update_fields.pop("address_detail", None)
|
||||
if address_detail:
|
||||
# Map AddressIn fields to ServiceProvider flat columns
|
||||
addr_mapping = {
|
||||
"zip": "address_zip",
|
||||
"city": None, # city is a direct column, handled separately
|
||||
"street_name": "address_street_name",
|
||||
"street_type": "address_street_type",
|
||||
"house_number": "address_house_number",
|
||||
"stairwell": "address_stairwell",
|
||||
"floor": "address_floor",
|
||||
"door": "address_door",
|
||||
"parcel_id": "address_hrsz",
|
||||
"full_address_text": None, # maps to address field
|
||||
"latitude": None, # maps to latitude
|
||||
"longitude": None, # maps to longitude
|
||||
}
|
||||
for addr_field, model_field in addr_mapping.items():
|
||||
if addr_field in address_detail and address_detail[addr_field] is not None:
|
||||
if model_field:
|
||||
update_fields[model_field] = address_detail[addr_field]
|
||||
elif addr_field == "full_address_text":
|
||||
update_fields["address"] = address_detail[addr_field]
|
||||
elif addr_field == "latitude":
|
||||
update_fields["latitude"] = address_detail[addr_field]
|
||||
elif addr_field == "longitude":
|
||||
update_fields["longitude"] = address_detail[addr_field]
|
||||
elif addr_field == "city":
|
||||
update_fields["city"] = address_detail[addr_field]
|
||||
|
||||
# =====================================================================
|
||||
# STEP 1: Extract multi-dimensional arrays and JSONB from payload
|
||||
# =====================================================================
|
||||
@@ -1298,10 +1336,13 @@ async def update_provider(
|
||||
name=provider.name,
|
||||
address=provider.address or "",
|
||||
city=provider.city,
|
||||
address_zip=provider.address_zip,
|
||||
address_street_name=provider.address_street_name,
|
||||
address_street_type=provider.address_street_type,
|
||||
address_house_number=provider.address_house_number,
|
||||
address_detail=AddressOut(
|
||||
zip=provider.address_zip,
|
||||
city=provider.city,
|
||||
street_name=provider.address_street_name,
|
||||
street_type=provider.address_street_type,
|
||||
house_number=provider.address_house_number,
|
||||
),
|
||||
plus_code=provider.plus_code,
|
||||
contact_phone=provider.contact_phone,
|
||||
contact_email=provider.contact_email,
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.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__)
|
||||
@@ -567,20 +568,42 @@ async def create_expense(
|
||||
# ── PROVIDER AUTO-DISCOVERY HOOK (Card #360) ──
|
||||
# Ha external_vendor_name meg van adva, de service_provider_id nincs,
|
||||
# automatikusan felfedezzük vagy létrehozzuk a providert.
|
||||
# A find_or_create_provider_by_name() kezeli a gamification pontok kiosztását is
|
||||
# (PROVIDER_DISCOVERY / PROVIDER_CONFIRMATION / PROVIDER_VERIFIED_USE).
|
||||
#
|
||||
# P0 ARCHITECTURE CLEANUP (2026-07-01):
|
||||
# A gamification/XP logika ELTÁVOLÍTVA a service rétegből.
|
||||
# A find_or_create_provider_by_name() már csak (provider, created) tuple-t ad vissza.
|
||||
# Ha created=True (új provider felfedezve), itt az API rétegben hívjuk meg
|
||||
# a gamification_service.award_points()-t a PROVIDER_DISCOVERY action_key-kel.
|
||||
resolved_provider_id = expense.service_provider_id
|
||||
if expense.external_vendor_name and not expense.service_provider_id:
|
||||
try:
|
||||
provider, action_key = await find_or_create_provider_by_name(
|
||||
provider, created = await find_or_create_provider_by_name(
|
||||
db=db,
|
||||
name=expense.external_vendor_name,
|
||||
added_by_user_id=current_user.id,
|
||||
)
|
||||
resolved_provider_id = provider.id
|
||||
|
||||
# P0 ARCHITECTURE CLEANUP: Gamification XP kiosztása az API rétegben
|
||||
# Csak akkor adunk XP-t, ha a provider újonnan lett felfedezve (created=True)
|
||||
if created:
|
||||
await gamification_service.award_points(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
amount=0,
|
||||
reason="PROVIDER_DISCOVERY",
|
||||
commit=False, # A hívó (create_expense) kezeli a commit-ot
|
||||
action_key="PROVIDER_DISCOVERY",
|
||||
)
|
||||
logger.info(
|
||||
f"Gamification XP awarded for PROVIDER_DISCOVERY: "
|
||||
f"name='{expense.external_vendor_name}', "
|
||||
f"provider_id={provider.id}, user_id={current_user.id}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Provider auto-discovery: name='{expense.external_vendor_name}', "
|
||||
f"provider_id={provider.id}, action_key='{action_key}', "
|
||||
f"provider_id={provider.id}, created={created}, "
|
||||
f"user_id={current_user.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -17,9 +17,12 @@ from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse, OrganizationUpdate, OrganizationResponse
|
||||
from app.schemas.subscription import SubscriptionTierResponse
|
||||
from app.schemas.address import AddressOut
|
||||
from app.services.billing_engine import upgrade_org_subscription
|
||||
from app.services.address_manager import AddressManager
|
||||
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember, Branch, OrgUserRole, OrgRole
|
||||
from app.models.identity import User, OneTimePassword
|
||||
from app.models.identity.address import Address
|
||||
from app.core.config import settings
|
||||
from app.services.security_service import security_service
|
||||
from app.models import LogSeverity
|
||||
@@ -49,7 +52,7 @@ async def onboard_organization(
|
||||
if org_in.country_code == "HU":
|
||||
if not re.match(r"^\d{8}-\d-\d{2}$", org_in.tax_number):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=_("ORGANIZATION.ERROR.INVALID_TAX_FORMAT")
|
||||
)
|
||||
|
||||
@@ -65,7 +68,10 @@ async def onboard_organization(
|
||||
# 3. KÖTELEZŐ MEZŐ: folder_slug generálása
|
||||
temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12]
|
||||
|
||||
# 4. Mentés
|
||||
# 4. P0 REFACTORED: Create Address record via AddressManager, then link via address_id FK
|
||||
address_id = await AddressManager.create_or_update(db, None, org_in.address)
|
||||
|
||||
# 5. Mentés
|
||||
new_org = Organization(
|
||||
full_name=org_in.full_name,
|
||||
name=org_in.name,
|
||||
@@ -73,12 +79,7 @@ async def onboard_organization(
|
||||
tax_number=org_in.tax_number,
|
||||
reg_number=org_in.reg_number,
|
||||
folder_slug=temp_slug,
|
||||
address_zip=org_in.address_zip,
|
||||
address_city=org_in.address_city,
|
||||
address_street_name=org_in.address_street_name,
|
||||
address_street_type=org_in.address_street_type,
|
||||
address_house_number=org_in.address_house_number,
|
||||
address_hrsz=org_in.address_hrsz,
|
||||
address_id=address_id,
|
||||
country_code=org_in.country_code,
|
||||
org_type=OrgType.business,
|
||||
status="pending_verification",
|
||||
@@ -96,22 +97,17 @@ async def onboard_organization(
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
|
||||
# 5. ALAPÉRTELMEZETT KÖZPONTI TELEPHELY LÉTREHOZÁSA
|
||||
# 6. ALAPÉRTELMEZETT KÖZPONTI TELEPHELY LÉTREHOZÁSA (with address_id FK)
|
||||
main_branch = Branch(
|
||||
organization_id=new_org.id,
|
||||
name=_("ORGANIZATION.BRANCH.DEFAULT_NAME"),
|
||||
is_main=True,
|
||||
postal_code=org_in.address_zip,
|
||||
city=org_in.address_city,
|
||||
street_name=org_in.address_street_name,
|
||||
street_type=org_in.address_street_type,
|
||||
house_number=org_in.address_house_number,
|
||||
hrsz=org_in.address_hrsz,
|
||||
address_id=address_id,
|
||||
status="active"
|
||||
)
|
||||
db.add(main_branch)
|
||||
|
||||
# 6. TULAJDONOS RÖGZÍTÉSE
|
||||
# 7. TULAJDONOS RÖGZÍTÉSE
|
||||
owner_member = OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=current_user.id,
|
||||
@@ -119,7 +115,7 @@ async def onboard_organization(
|
||||
)
|
||||
db.add(owner_member)
|
||||
|
||||
# 7. NAS Mappa létrehozása
|
||||
# 8. NAS Mappa létrehozása
|
||||
try:
|
||||
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
||||
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
||||
@@ -185,6 +181,7 @@ async def get_my_organizations(
|
||||
A bejelentkezett felhasználóhoz tartozó szervezetek listázása.
|
||||
P0: Minden szervezethez visszaadja a valós max_vehicles és max_garages limiteket
|
||||
a subscription_tier JSONB rules alapján.
|
||||
P0 REFACTORED: Address adatok az address relációból (AddressOut séma).
|
||||
"""
|
||||
from app.models.core_logic import SubscriptionTier, OrganizationSubscription
|
||||
|
||||
@@ -205,7 +202,10 @@ async def get_my_organizations(
|
||||
stmt = (
|
||||
select(Organization)
|
||||
.where(Organization.id.in_(select(subq.c.id)))
|
||||
.options(selectinload(Organization.members))
|
||||
.options(
|
||||
selectinload(Organization.members),
|
||||
selectinload(Organization.address),
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
orgs = result.scalars().all()
|
||||
@@ -256,13 +256,8 @@ async def get_my_organizations(
|
||||
"org_type": o.org_type.value if hasattr(o.org_type, 'value') else str(o.org_type),
|
||||
"visual_settings": o.visual_settings if hasattr(o, 'visual_settings') else None,
|
||||
"user_role": _get_user_role(o, current_user.id),
|
||||
# ── Cím adatok (Address fields) ──
|
||||
"address_zip": o.address_zip,
|
||||
"address_city": o.address_city,
|
||||
"address_street_name": o.address_street_name,
|
||||
"address_street_type": o.address_street_type,
|
||||
"address_house_number": o.address_house_number,
|
||||
"address_hrsz": o.address_hrsz,
|
||||
# ── P0 REFACTORED: Address from relationship ──
|
||||
"address": AddressOut.model_validate(o.address).model_dump() if o.address else None,
|
||||
}
|
||||
for o in orgs
|
||||
]
|
||||
@@ -704,6 +699,12 @@ async def update_organization(
|
||||
"""
|
||||
Szervezet adatainak részleges frissítése (általános PATCH).
|
||||
Csak OWNER vagy ADMIN jogosultságú felhasználó módosíthatja.
|
||||
|
||||
P0 REFACTORED: Address kezelés a unified AddressIn/AddressOut séma alapján.
|
||||
- Ha payload.address meg van adva, akkor:
|
||||
* Ha van meglévő address_id → frissíti a system.addresses rekordot
|
||||
* Ha nincs → létrehoz egy új system.addresses rekordot és beállítja az address_id FK-t
|
||||
- A régi denormalizált mezők (address_zip, address_city, stb.) ELTÁVOLÍTVA.
|
||||
"""
|
||||
# 1. Jogosultság ellenőrzése
|
||||
# Először lekérjük a szervezetet, hogy ellenőrizhessük az owner_id-t is
|
||||
@@ -745,7 +746,15 @@ async def update_organization(
|
||||
org.visual_settings = current_vs
|
||||
del update_dict["visual_settings"]
|
||||
|
||||
# 4. Többi mező frissítése
|
||||
# 3. P0 REFACTORED: Address FK logic via AddressManager
|
||||
if "address" in update_dict:
|
||||
addr_in = update_dict["address"]
|
||||
org.address_id = await AddressManager.create_or_update(
|
||||
db, org.address_id, addr_in
|
||||
)
|
||||
del update_dict["address"]
|
||||
|
||||
# 4. Többi mező frissítése (kivéve a régi denormalizált címmezőket)
|
||||
for field, value in update_dict.items():
|
||||
if hasattr(org, field) and value is not None:
|
||||
setattr(org, field, value)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, or_
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.marketplace.organization import Organization, Branch
|
||||
from app.models.identity.address import Address, GeoPostalCode
|
||||
from geoalchemy2 import WKTElement
|
||||
from typing import Optional
|
||||
|
||||
@@ -23,16 +24,24 @@ async def match_service(
|
||||
"""
|
||||
Geofencing keresőmotor PostGIS segítségével.
|
||||
Ha nincs megadva lat/lng, akkor nem alkalmazunk távolságszűrést.
|
||||
|
||||
P0 UNIFIED ADDRESS REFACTOR (2026-07-01): A Branch.city denormalizált
|
||||
mező helyett az Address → GeoPostalCode kapcsolaton keresztül érjük el
|
||||
a város adatokat. LEFT JOIN-t használunk, mert az address_id lehet NULL.
|
||||
"""
|
||||
# Alap lekérdezés: aktív szervezetek és telephelyek
|
||||
query = select(
|
||||
Organization.id,
|
||||
Organization.name,
|
||||
Branch.city,
|
||||
GeoPostalCode.city.label("city"),
|
||||
Branch.branch_rating,
|
||||
Branch.location
|
||||
).join(
|
||||
Branch, Organization.id == Branch.organization_id
|
||||
).outerjoin(
|
||||
Address, Address.id == Branch.address_id
|
||||
).outerjoin(
|
||||
GeoPostalCode, GeoPostalCode.id == Address.postal_code_id
|
||||
).where(
|
||||
Organization.is_active == True,
|
||||
Branch.is_deleted == False
|
||||
|
||||
@@ -12,6 +12,7 @@ from datetime import datetime
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate, UserWithTokenResponse, PersonUpdate, ChangePasswordRequest
|
||||
from app.schemas.address import AddressOut
|
||||
from app.models.identity import User, Person
|
||||
from app.models.identity.address import Address
|
||||
from app.services.trust_engine import TrustEngine
|
||||
@@ -69,20 +70,7 @@ async def _build_user_response(user: User, active_org_id: Optional[int] = None,
|
||||
if person:
|
||||
address_data = None
|
||||
if person.address:
|
||||
address_data = {
|
||||
"address_zip": getattr(person.address, 'zip', None) or getattr(getattr(person.address, 'postal_code', None), 'zip_code', None),
|
||||
"address_city": getattr(person.address, 'city', None) or getattr(getattr(person.address, 'postal_code', None), 'city', None),
|
||||
"address_street_name": person.address.street_name,
|
||||
"address_street_type": person.address.street_type,
|
||||
"address_house_number": person.address.house_number,
|
||||
"address_stairwell": person.address.stairwell,
|
||||
"address_floor": person.address.floor,
|
||||
"address_door": person.address.door,
|
||||
"address_hrsz": person.address.parcel_id,
|
||||
"full_address_text": person.address.full_address_text,
|
||||
"latitude": person.address.latitude,
|
||||
"longitude": person.address.longitude,
|
||||
}
|
||||
address_data = AddressOut.model_validate(person.address)
|
||||
person_data = {
|
||||
"id": person.id,
|
||||
"id_uuid": str(person.id_uuid) if person.id_uuid else None,
|
||||
@@ -343,14 +331,10 @@ async def update_my_person(
|
||||
raise HTTPException(status_code=400, detail="No Person record found. Complete KYC first.")
|
||||
|
||||
# ── Update Person fields ──
|
||||
update_dict = update_data.dict(exclude_unset=True)
|
||||
update_dict = update_data.model_dump(exclude_unset=True)
|
||||
|
||||
person_fields = ["first_name", "last_name", "phone", "mothers_last_name",
|
||||
"mothers_first_name", "birth_place", "birth_date"]
|
||||
address_fields = ["address_zip", "address_city", "address_street_name",
|
||||
"address_street_type", "address_house_number",
|
||||
"address_stairwell", "address_floor", "address_door",
|
||||
"address_hrsz"]
|
||||
|
||||
for field in person_fields:
|
||||
if field in update_dict and update_dict[field] is not None:
|
||||
@@ -368,21 +352,21 @@ async def update_my_person(
|
||||
person.identity_docs = update_dict["identity_docs"]
|
||||
flag_modified(person, "identity_docs")
|
||||
|
||||
# ── Update Address fields ──
|
||||
has_address_update = any(f in update_dict for f in address_fields)
|
||||
if has_address_update:
|
||||
# ── Update Address fields (via AddressIn) ──
|
||||
addr_data = update_data.address
|
||||
if addr_data is not None:
|
||||
# Use GeoService to get or create address
|
||||
addr_id = await GeoService.get_or_create_full_address(
|
||||
db,
|
||||
zip_code=update_dict.get("address_zip"),
|
||||
city=update_dict.get("address_city"),
|
||||
street_name=update_dict.get("address_street_name"),
|
||||
street_type=update_dict.get("address_street_type"),
|
||||
house_number=update_dict.get("address_house_number"),
|
||||
stairwell=update_dict.get("address_stairwell"),
|
||||
floor=update_dict.get("address_floor"),
|
||||
door=update_dict.get("address_door"),
|
||||
parcel_id=update_dict.get("address_hrsz"),
|
||||
zip_code=addr_data.zip,
|
||||
city=addr_data.city,
|
||||
street_name=addr_data.street_name,
|
||||
street_type=addr_data.street_type,
|
||||
house_number=addr_data.house_number,
|
||||
stairwell=addr_data.stairwell,
|
||||
floor=addr_data.floor,
|
||||
door=addr_data.door,
|
||||
parcel_id=addr_data.parcel_id,
|
||||
)
|
||||
if addr_id:
|
||||
person.address_id = addr_id
|
||||
|
||||
Reference in New Issue
Block a user