admin frontend elkezdése, járművek tisztázása, frontend fejleszts
This commit is contained in:
31
backend/scripts/check_constraint.py
Normal file
31
backend/scripts/check_constraint.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Check and create the CheckConstraint for vehicle.assets table."""
|
||||
from app.database import engine_sync
|
||||
from sqlalchemy import text
|
||||
|
||||
with engine_sync.connect() as conn:
|
||||
# Check if constraint exists
|
||||
result = conn.execute(
|
||||
text("""
|
||||
SELECT conname, pg_get_constraintdef(oid)
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'vehicle.assets'::regclass
|
||||
AND contype = 'c'
|
||||
""")
|
||||
).fetchall()
|
||||
print('Constraints on vehicle.assets:')
|
||||
for row in result:
|
||||
print(f' {row[0]}: {row[1]}')
|
||||
|
||||
if not result:
|
||||
print('No CheckConstraint found - creating it now...')
|
||||
conn.execute(
|
||||
text("""
|
||||
ALTER TABLE vehicle.assets
|
||||
ADD CONSTRAINT ck_asset_vin_or_plate_required
|
||||
CHECK (vin IS NOT NULL OR license_plate IS NOT NULL)
|
||||
""")
|
||||
)
|
||||
conn.commit()
|
||||
print('CheckConstraint created successfully!')
|
||||
else:
|
||||
print('CheckConstraint already exists.')
|
||||
249
backend/scripts/repair_kyc_user.py
Normal file
249
backend/scripts/repair_kyc_user.py
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Repair script for user gy.krisztina76@gmail.com (ID 100).
|
||||
This user went through light registration + email verification but the KYC
|
||||
completion failed because of two bugs:
|
||||
1. verify_email() used Person.user_id (NULL) instead of User.person_id
|
||||
2. GamificationService.award_points() did internal commit conflicting with outer transaction
|
||||
|
||||
This script manually creates all the missing infrastructure that should have
|
||||
been created by complete_kyc():
|
||||
- Activate Person
|
||||
- Create Organization (personal)
|
||||
- Create Branch
|
||||
- Create OrganizationMember
|
||||
- Create Wallet
|
||||
- Create UserStats
|
||||
- Set user.scope_id
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Add the backend directory to the path
|
||||
sys.path.insert(0, '/app/backend')
|
||||
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
logger = logging.getLogger("repair_kyc")
|
||||
|
||||
# Database URL from settings
|
||||
DATABASE_URL = settings.DATABASE_URL
|
||||
logger.info(f"Using database: {DATABASE_URL}")
|
||||
|
||||
# Create engine and session
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def repair_user():
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# 1. Find the user
|
||||
from app.models.identity.identity import User, Person, Wallet
|
||||
from app.models.marketplace.organization import Organization, OrganizationMember, Branch
|
||||
from app.models.gamification.gamification import UserStats, PointsLedger
|
||||
|
||||
stmt = select(User).where(User.email == 'gy.krisztina76@gmail.com')
|
||||
user = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
logger.error("User not found!")
|
||||
return
|
||||
|
||||
logger.info(f"Found user: ID={user.id}, email={user.email}, person_id={user.person_id}")
|
||||
logger.info(f" is_active={user.is_active}, scope_id={user.scope_id}")
|
||||
|
||||
# 2. Find and activate Person
|
||||
person_stmt = select(Person).where(Person.id == user.person_id)
|
||||
person = (await db.execute(person_stmt)).scalar_one_or_none()
|
||||
|
||||
if person:
|
||||
logger.info(f"Found Person: ID={person.id}, is_active={person.is_active}, user_id={person.user_id}")
|
||||
person.is_active = True
|
||||
person.user_id = user.id
|
||||
logger.info(f" -> Activated Person, set user_id={user.id}")
|
||||
else:
|
||||
logger.warning(f"Person with id={user.person_id} not found!")
|
||||
|
||||
# 3. Check if Organization already exists
|
||||
org_stmt = select(Organization).where(Organization.owner_id == user.id)
|
||||
existing_org = (await db.execute(org_stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_org:
|
||||
logger.info(f"Organization already exists: ID={existing_org.id}, name={existing_org.name}")
|
||||
new_org = existing_org
|
||||
else:
|
||||
# Create personal Organization
|
||||
from app.core.security import generate_secure_slug
|
||||
|
||||
org_name = f"{person.last_name} Flotta" if person else "Personal Flotta"
|
||||
folder_slug = generate_secure_slug(12)
|
||||
|
||||
new_org = Organization(
|
||||
name=org_name,
|
||||
full_name=org_name,
|
||||
display_name=org_name,
|
||||
folder_slug=folder_slug,
|
||||
org_type="individual",
|
||||
status="ACTIVE",
|
||||
is_deleted=False,
|
||||
is_active=True,
|
||||
is_verified=False,
|
||||
is_anonymized=False,
|
||||
default_currency="HUF",
|
||||
country_code="HU",
|
||||
language="hu",
|
||||
subscription_plan="FREE",
|
||||
base_asset_limit=3,
|
||||
purchased_extra_slots=0,
|
||||
notification_settings={},
|
||||
external_integration_config={},
|
||||
visual_settings={},
|
||||
is_ownership_transferable=False,
|
||||
owner_id=user.id,
|
||||
first_registered_at=datetime.now(timezone.utc),
|
||||
current_lifecycle_started_at=datetime.now(timezone.utc),
|
||||
lifecycle_index=1,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
logger.info(f"Created Organization: ID={new_org.id}, name={org_name}")
|
||||
|
||||
# 4. Check if Branch already exists
|
||||
branch_stmt = select(Branch).where(Branch.organization_id == new_org.id)
|
||||
existing_branch = (await db.execute(branch_stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_branch:
|
||||
logger.info(f"Branch already exists: ID={existing_branch.id}, name={existing_branch.name}")
|
||||
else:
|
||||
import uuid
|
||||
branch = Branch(
|
||||
id=uuid.uuid4(),
|
||||
organization_id=new_org.id,
|
||||
name="Home Base",
|
||||
is_main=True,
|
||||
opening_hours={},
|
||||
branch_rating=0.0,
|
||||
status="ACTIVE",
|
||||
is_deleted=False,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(branch)
|
||||
await db.flush()
|
||||
logger.info(f"Created Branch: ID={branch.id}")
|
||||
|
||||
# 5. Check if OrganizationMember already exists
|
||||
member_stmt = select(OrganizationMember).where(
|
||||
and_(
|
||||
OrganizationMember.organization_id == new_org.id,
|
||||
OrganizationMember.user_id == user.id
|
||||
)
|
||||
)
|
||||
existing_member = (await db.execute(member_stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_member:
|
||||
logger.info(f"OrganizationMember already exists: ID={existing_member.id}")
|
||||
else:
|
||||
member = OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=user.id,
|
||||
role="OWNER",
|
||||
permissions={},
|
||||
is_permanent=True,
|
||||
is_verified=True
|
||||
)
|
||||
db.add(member)
|
||||
await db.flush()
|
||||
logger.info(f"Created OrganizationMember: user_id={user.id}, org_id={new_org.id}")
|
||||
|
||||
# 6. Check if Wallet already exists
|
||||
wallet_stmt = select(Wallet).where(Wallet.user_id == user.id)
|
||||
existing_wallet = (await db.execute(wallet_stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_wallet:
|
||||
logger.info(f"Wallet already exists: ID={existing_wallet.id}")
|
||||
else:
|
||||
wallet = Wallet(
|
||||
user_id=user.id,
|
||||
earned_credits=0,
|
||||
purchased_credits=0,
|
||||
service_coins=0,
|
||||
currency="HUF"
|
||||
)
|
||||
db.add(wallet)
|
||||
await db.flush()
|
||||
logger.info(f"Created Wallet for user_id={user.id}")
|
||||
|
||||
# 7. Check if UserStats already exists
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == user.id)
|
||||
existing_stats = (await db.execute(stats_stmt)).scalar_one_or_none()
|
||||
|
||||
if existing_stats:
|
||||
logger.info(f"UserStats already exists: user_id={existing_stats.user_id}")
|
||||
else:
|
||||
stats = UserStats(
|
||||
user_id=user.id,
|
||||
total_xp=500, # KYC bonus
|
||||
social_points=0,
|
||||
current_level=1,
|
||||
penalty_points=0,
|
||||
restriction_level=0,
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
penalty_quota_remaining=0,
|
||||
places_discovered=0,
|
||||
places_validated=0
|
||||
)
|
||||
db.add(stats)
|
||||
|
||||
# Add PointsLedger entry for KYC bonus
|
||||
ledger = PointsLedger(
|
||||
user_id=user.id,
|
||||
points=500,
|
||||
reason="KYC_VERIFICATION_REPAIR"
|
||||
)
|
||||
db.add(ledger)
|
||||
await db.flush()
|
||||
logger.info(f"Created UserStats and PointsLedger for user_id={user.id}")
|
||||
|
||||
# 8. Set user.scope_id if not set
|
||||
if not user.scope_id:
|
||||
user.scope_id = str(new_org.id)
|
||||
logger.info(f"Set user.scope_id = {new_org.id}")
|
||||
|
||||
# 9. Ensure user.is_active is True
|
||||
if not user.is_active:
|
||||
user.is_active = True
|
||||
logger.info("Activated user")
|
||||
|
||||
await db.commit()
|
||||
logger.info("=" * 50)
|
||||
logger.info("REPAIR COMPLETED SUCCESSFULLY!")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# Verify
|
||||
logger.info("\n--- Verification ---")
|
||||
logger.info(f"User: ID={user.id}, is_active={user.is_active}, scope_id={user.scope_id}")
|
||||
if person:
|
||||
logger.info(f"Person: ID={person.id}, is_active={person.is_active}, user_id={person.user_id}")
|
||||
logger.info(f"Organization: ID={new_org.id}, name={new_org.name}")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Repair failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
await db.close()
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(repair_user())
|
||||
97
backend/scripts/seed_cost_category_tiers.py
Normal file
97
backend/scripts/seed_cost_category_tiers.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Seed script: CostCategory min_tier beállítása.
|
||||
|
||||
A szkript beállítja a költségkategóriák min_tier mezőjét a kód alapján.
|
||||
A tier-ek hierarchikusak: free < premium < enterprise.
|
||||
|
||||
Kategória tier hozzárendelés:
|
||||
- FUEL, SERVICE, TIRES, INSURANCE, TAXES -> free
|
||||
- TOLL_PARKING, CLEANING, OTHER -> premium
|
||||
- (minden más) -> enterprise
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python -m backend.scripts.seed_cost_category_tiers
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.vehicle.vehicle import CostCategory
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Tier mapping by category code
|
||||
# A fő kategóriák (parent_id IS NULL) tier beállításai.
|
||||
# Az alkategóriák (pl. FUEL_PETROL, MAINT_SERVICE) öröklik a szülő tier-jét.
|
||||
CATEGORY_TIER_MAP = {
|
||||
"FUEL": "free", # Üzemanyag / Töltés
|
||||
"MAINTENANCE": "free", # Szerviz & Karbantartás
|
||||
"TIRES": "free", # Gumiabroncsok
|
||||
"INSURANCE": "free", # Biztosítás
|
||||
"TAX": "free", # Adók
|
||||
"FEES": "premium", # Útdíj & Parkolás
|
||||
"ADMIN": "premium", # Hatósági díjak
|
||||
"FINANCE": "premium", # Finanszírozás
|
||||
"CLEANING": "premium", # Ápolás & Kozmetika
|
||||
"OTHER": "premium", # Egyéb
|
||||
}
|
||||
|
||||
# Default tier for categories not in the map
|
||||
DEFAULT_TIER = "enterprise"
|
||||
|
||||
|
||||
async def seed_cost_category_tiers():
|
||||
"""Beállítja a min_tier értékeket az összes CostCategory rekordra."""
|
||||
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
# Lekérjük az összes kategóriát
|
||||
stmt = select(CostCategory).order_by(CostCategory.id)
|
||||
result = await db.execute(stmt)
|
||||
categories = result.scalars().all()
|
||||
|
||||
if not categories:
|
||||
logger.warning("❌ Nincsenek CostCategory rekordok az adatbázisban!")
|
||||
return
|
||||
|
||||
updated_count = 0
|
||||
for cat in categories:
|
||||
# Meghatározzuk a tier-t a kód alapján
|
||||
new_tier = CATEGORY_TIER_MAP.get(cat.code, DEFAULT_TIER)
|
||||
|
||||
# Csak akkor frissítjük, ha még nincs beállítva, vagy változott
|
||||
if cat.min_tier != new_tier:
|
||||
cat.min_tier = new_tier
|
||||
updated_count += 1
|
||||
logger.info(
|
||||
f" ✅ [{cat.code:15s}] {cat.name:30s} -> tier={new_tier}"
|
||||
)
|
||||
|
||||
if updated_count > 0:
|
||||
await db.commit()
|
||||
logger.info(f"\n✅ {updated_count} kategória tier beállítva.")
|
||||
else:
|
||||
logger.info("\n✅ Minden kategória tier már be van állítva.")
|
||||
|
||||
# Összesítő
|
||||
logger.info("\n📊 Tier összesítő:")
|
||||
for cat in categories:
|
||||
logger.info(f" [{cat.min_tier:10s}] {cat.code:15s} - {cat.name}")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Hiba a seedelés során: {e}")
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_cost_category_tiers())
|
||||
245
backend/scripts/test_kyc_e2e.py
Normal file
245
backend/scripts/test_kyc_e2e.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
E2E Test: Light Registration → Email Verification → KYC Completion
|
||||
Verifies that all necessary database relationships are created after KYC.
|
||||
"""
|
||||
import asyncio
|
||||
import httpx
|
||||
import uuid
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.db.session import AsyncSessionLocal
|
||||
|
||||
BASE_URL = "http://localhost:8000/api/v1"
|
||||
TEST_EMAIL = f"test_kyc_{uuid.uuid4().hex[:8]}@example.com"
|
||||
TEST_PASSWORD = "TestPass123!"
|
||||
|
||||
async def get_verification_token(email: str):
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
text("SELECT token FROM identity.verification_tokens vt "
|
||||
"JOIN identity.users u ON u.id = vt.user_id "
|
||||
"WHERE u.email = :email ORDER BY vt.created_at DESC LIMIT 1"),
|
||||
{"email": email}
|
||||
)
|
||||
return result.scalar()
|
||||
|
||||
async def check_infrastructure(user_id: int):
|
||||
"""Check all required infrastructure objects exist."""
|
||||
async with AsyncSessionLocal() as session:
|
||||
checks = {}
|
||||
|
||||
# Person
|
||||
r = await session.execute(
|
||||
text("SELECT id, is_active, user_id FROM identity.persons WHERE id = "
|
||||
"(SELECT person_id FROM identity.users WHERE id = :uid)"),
|
||||
{"uid": user_id}
|
||||
)
|
||||
person = r.fetchone()
|
||||
checks["person"] = person is not None
|
||||
if person:
|
||||
checks["person_active"] = person[1] == True
|
||||
checks["person_user_id"] = person[2] == user_id
|
||||
|
||||
# Organization
|
||||
r = await session.execute(
|
||||
text("SELECT id, name, org_type FROM fleet.organizations WHERE owner_id = :uid"),
|
||||
{"uid": user_id}
|
||||
)
|
||||
org = r.fetchone()
|
||||
checks["organization"] = org is not None
|
||||
if org:
|
||||
checks["org_id"] = org[0]
|
||||
|
||||
# Branch
|
||||
if org:
|
||||
r = await session.execute(
|
||||
text("SELECT id, name, is_main FROM fleet.branches WHERE organization_id = :oid"),
|
||||
{"oid": org[0]}
|
||||
)
|
||||
branch = r.fetchone()
|
||||
checks["branch"] = branch is not None
|
||||
if branch:
|
||||
checks["branch_main"] = branch[2] == True
|
||||
|
||||
# OrganizationMember
|
||||
r = await session.execute(
|
||||
text("SELECT id, role FROM fleet.organization_members WHERE user_id = :uid"),
|
||||
{"uid": user_id}
|
||||
)
|
||||
member = r.fetchone()
|
||||
checks["org_member"] = member is not None
|
||||
if member:
|
||||
checks["org_member_role"] = member[1] == "OWNER"
|
||||
|
||||
# Wallet
|
||||
r = await session.execute(
|
||||
text("SELECT id FROM identity.wallets WHERE user_id = :uid"),
|
||||
{"uid": user_id}
|
||||
)
|
||||
checks["wallet"] = r.fetchone() is not None
|
||||
|
||||
# UserStats
|
||||
r = await session.execute(
|
||||
text("SELECT user_id, total_xp, current_level FROM gamification.user_stats WHERE user_id = :uid"),
|
||||
{"uid": user_id}
|
||||
)
|
||||
stats = r.fetchone()
|
||||
checks["user_stats"] = stats is not None
|
||||
if stats:
|
||||
checks["user_stats_xp"] = stats[1] >= 500 # KYC bonus
|
||||
checks["user_stats_level"] = stats[2] >= 1
|
||||
|
||||
# User scope_id
|
||||
r = await session.execute(
|
||||
text("SELECT scope_id FROM identity.users WHERE id = :uid"),
|
||||
{"uid": user_id}
|
||||
)
|
||||
scope = r.fetchone()
|
||||
checks["scope_id_set"] = scope is not None and scope[0] is not None
|
||||
|
||||
return checks
|
||||
|
||||
async def run_test():
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
print("=" * 60)
|
||||
print(f"KYC E2E TEST - Email: {TEST_EMAIL}")
|
||||
print("=" * 60)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# --- STEP 1: LITE REGISTRATION ---
|
||||
print("\n--- 1. LITE REGISTRATION ---")
|
||||
reg_payload = {
|
||||
"email": TEST_EMAIL,
|
||||
"password": TEST_PASSWORD,
|
||||
"first_name": "KYC",
|
||||
"last_name": "TestUser",
|
||||
"region_code": "HU"
|
||||
}
|
||||
resp = await client.post(f"{BASE_URL}/auth/register", json=reg_payload)
|
||||
print(f"Status: {resp.status_code}")
|
||||
if resp.status_code == 201:
|
||||
print("✓ Registration successful")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ Registration failed: {resp.text}")
|
||||
failed += 1
|
||||
return passed, failed
|
||||
|
||||
# --- STEP 2: EMAIL VERIFICATION ---
|
||||
print("\n--- 2. EMAIL VERIFICATION ---")
|
||||
token = await get_verification_token(TEST_EMAIL)
|
||||
if not token:
|
||||
print("✗ No verification token found!")
|
||||
failed += 1
|
||||
return passed, failed
|
||||
|
||||
print(f"Token: {token}")
|
||||
verify_resp = await client.post(f"{BASE_URL}/auth/verify-email", json={"token": str(token)})
|
||||
print(f"Status: {verify_resp.status_code}")
|
||||
if verify_resp.status_code == 200:
|
||||
print("✓ Email verification successful")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ Verification failed: {verify_resp.text}")
|
||||
failed += 1
|
||||
return passed, failed
|
||||
|
||||
# --- STEP 3: LOGIN TO GET TOKEN ---
|
||||
print("\n--- 3. LOGIN ---")
|
||||
login_data = {
|
||||
"username": TEST_EMAIL,
|
||||
"password": TEST_PASSWORD,
|
||||
"grant_type": "password"
|
||||
}
|
||||
login_resp = await client.post(f"{BASE_URL}/auth/login", data=login_data)
|
||||
print(f"Status: {login_resp.status_code}")
|
||||
if login_resp.status_code == 200:
|
||||
print("✓ Login successful")
|
||||
passed += 1
|
||||
access_token = login_resp.json().get("access_token")
|
||||
else:
|
||||
print(f"✗ Login failed: {login_resp.text}")
|
||||
failed += 1
|
||||
return passed, failed
|
||||
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
# --- STEP 4: GET USER INFO ---
|
||||
print("\n--- 4. GET USER INFO ---")
|
||||
me_resp = await client.get(f"{BASE_URL}/auth/me", headers=headers)
|
||||
print(f"Status: {me_resp.status_code}")
|
||||
if me_resp.status_code == 200:
|
||||
user_data = me_resp.json()
|
||||
user_id = user_data.get("id")
|
||||
print(f"User ID: {user_id}")
|
||||
print(f"User data: {user_data}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ Failed to get user info: {me_resp.text}")
|
||||
failed += 1
|
||||
return passed, failed
|
||||
|
||||
# --- STEP 5: COMPLETE KYC ---
|
||||
print("\n--- 5. COMPLETE KYC ---")
|
||||
kyc_payload = {
|
||||
"first_name": "KYC",
|
||||
"last_name": "TestUser",
|
||||
"birth_date": "1990-01-15",
|
||||
"birth_place": "Budapest",
|
||||
"mothers_last_name": "Anyuka",
|
||||
"mothers_first_name": "Mária",
|
||||
"phone_number": "+36301234567",
|
||||
"address_zip": "1011",
|
||||
"address_city": "Budapest",
|
||||
"address_street_name": "Fő utca",
|
||||
"address_street_type": "utca",
|
||||
"address_house_number": "1",
|
||||
"preferred_currency": "HUF",
|
||||
"region_code": "HU",
|
||||
"identity_docs": {
|
||||
"PASSPORT": {"number": "AB123456", "expiry_date": "2030-01-01"}
|
||||
}
|
||||
}
|
||||
kyc_resp = await client.post(f"{BASE_URL}/auth/complete-kyc", json=kyc_payload, headers=headers)
|
||||
print(f"Status: {kyc_resp.status_code}")
|
||||
print(f"Response: {kyc_resp.text}")
|
||||
if kyc_resp.status_code == 200:
|
||||
print("✓ KYC completion successful")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ KYC failed: {kyc_resp.text}")
|
||||
failed += 1
|
||||
return passed, failed
|
||||
|
||||
# --- STEP 6: VERIFY INFRASTRUCTURE ---
|
||||
print("\n--- 6. VERIFY INFRASTRUCTURE ---")
|
||||
checks = await check_infrastructure(user_id)
|
||||
all_ok = True
|
||||
for check_name, check_result in checks.items():
|
||||
status = "✓" if check_result else "✗"
|
||||
if not check_result:
|
||||
all_ok = False
|
||||
print(f" {status} {check_name}: {check_result}")
|
||||
|
||||
if all_ok:
|
||||
print("✓ All infrastructure checks passed!")
|
||||
passed += 1
|
||||
else:
|
||||
print(f"✗ Some infrastructure checks failed!")
|
||||
failed += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"RESULTS: {passed} passed, {failed} failed")
|
||||
print("=" * 60)
|
||||
return passed, failed
|
||||
|
||||
if __name__ == "__main__":
|
||||
p, f = asyncio.run(run_test())
|
||||
sys.exit(1 if f > 0 else 0)
|
||||
269
backend/scripts/test_soft_delete_and_asset_validation.py
Normal file
269
backend/scripts/test_soft_delete_and_asset_validation.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Test script for:
|
||||
1. Asset VIN/License Plate OR-OR validation (Pydantic schemas)
|
||||
2. Person-Preserving Soft Delete logic
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
# ─── Test 1: Pydantic Schema Validation ───────────────────────────────────────
|
||||
print("=" * 70)
|
||||
print("🧪 TEST 1: Asset Pydantic OR-OR Validation")
|
||||
print("=" * 70)
|
||||
|
||||
from pydantic import ValidationError
|
||||
sys.path.insert(0, "/app")
|
||||
from app.schemas.asset import AssetCreate, AssetUpdate
|
||||
|
||||
def test_asset_create():
|
||||
"""Test AssetCreate schema validation."""
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
# Test 1.1: Both vin and plate provided - should pass
|
||||
try:
|
||||
a = AssetCreate(license_plate="ABC-123", vin="WDB1234567890ABCD")
|
||||
assert a.license_plate == "ABC-123"
|
||||
assert a.vin == "WDB1234567890ABCD"
|
||||
print(" ✅ 1.1 Both vin and plate provided: PASS")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.1 Both vin and plate provided: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 1.2: Only license_plate - should pass
|
||||
try:
|
||||
a = AssetCreate(license_plate="ABC-123")
|
||||
assert a.license_plate == "ABC-123"
|
||||
assert a.vin is None
|
||||
print(" ✅ 1.2 Only license_plate: PASS")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.2 Only license_plate: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 1.3: Only vin - should pass
|
||||
try:
|
||||
a = AssetCreate(vin="WDB1234567890ABCD")
|
||||
assert a.vin == "WDB1234567890ABCD"
|
||||
assert a.license_plate is None
|
||||
print(" ✅ 1.3 Only vin: PASS")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.3 Only vin: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 1.4: Neither vin nor plate - should FAIL
|
||||
try:
|
||||
a = AssetCreate()
|
||||
print(f" ❌ 1.4 Neither vin nor plate: FAIL (should have raised ValueError)")
|
||||
failed += 1
|
||||
except ValidationError as e:
|
||||
print(f" ✅ 1.4 Neither vin nor plate: PASS (correctly rejected)")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.4 Neither vin nor plate: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 1.5: Empty string license_plate - should be converted to None, then fail
|
||||
try:
|
||||
a = AssetCreate(license_plate="")
|
||||
print(f" ❌ 1.5 Empty string license_plate: FAIL (should have raised ValueError)")
|
||||
failed += 1
|
||||
except ValidationError as e:
|
||||
print(f" ✅ 1.5 Empty string license_plate: PASS (correctly rejected)")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.5 Empty string license_plate: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 1.6: Empty string vin - should be converted to None, then fail
|
||||
try:
|
||||
a = AssetCreate(vin=" ")
|
||||
print(f" ❌ 1.6 Whitespace-only vin: FAIL (should have raised ValueError)")
|
||||
failed += 1
|
||||
except ValidationError as e:
|
||||
print(f" ✅ 1.6 Whitespace-only vin: PASS (correctly rejected)")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.6 Whitespace-only vin: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 1.7: Both empty strings - should fail
|
||||
try:
|
||||
a = AssetCreate(license_plate="", vin="")
|
||||
print(f" ❌ 1.7 Both empty strings: FAIL (should have raised ValueError)")
|
||||
failed += 1
|
||||
except ValidationError as e:
|
||||
print(f" ✅ 1.7 Both empty strings: PASS (correctly rejected)")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 1.7 Both empty strings: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
return passed, failed
|
||||
|
||||
def test_asset_update():
|
||||
"""Test AssetUpdate schema validation."""
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
# Test 2.1: Update with only license_plate - should pass
|
||||
try:
|
||||
a = AssetUpdate(license_plate="NEW-999")
|
||||
assert a.license_plate == "NEW-999"
|
||||
print(" ✅ 2.1 Update with only license_plate: PASS")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 2.1 Update with only license_plate: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 2.2: Update with only vin - should pass
|
||||
try:
|
||||
a = AssetUpdate(vin="NEWVIN123456789")
|
||||
assert a.vin == "NEWVIN123456789"
|
||||
print(" ✅ 2.2 Update with only vin: PASS")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 2.2 Update with only vin: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 2.3: Update with both set to None explicitly - should FAIL
|
||||
try:
|
||||
a = AssetUpdate(license_plate=None, vin=None)
|
||||
print(f" ❌ 2.3 Both set to None: FAIL (should have raised ValueError)")
|
||||
failed += 1
|
||||
except ValidationError as e:
|
||||
print(f" ✅ 2.3 Both set to None: PASS (correctly rejected)")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 2.3 Both set to None: FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
# Test 2.4: Empty update (no fields) - should pass (exclude_unset)
|
||||
try:
|
||||
a = AssetUpdate()
|
||||
print(f" ✅ 2.4 Empty update (no fields): PASS")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 2.4 Empty update (no fields): FAIL - {e}")
|
||||
failed += 1
|
||||
|
||||
return passed, failed
|
||||
|
||||
# Run Pydantic tests
|
||||
p1, f1 = test_asset_create()
|
||||
p2, f2 = test_asset_update()
|
||||
print(f"\n📊 Asset Validation Results: {p1+p2} passed, {f1+f2} failed out of {p1+p2+f1+f2} tests")
|
||||
|
||||
# ─── Test 2: Soft Delete Logic ────────────────────────────────────────────────
|
||||
print("\n" + "=" * 70)
|
||||
print("🧪 TEST 2: Person-Preserving Soft Delete Logic")
|
||||
print("=" * 70)
|
||||
|
||||
async def test_soft_delete():
|
||||
"""Test the soft_delete_user method via direct DB calls."""
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.identity.identity import User, Person
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.security_service import security_service
|
||||
from sqlalchemy import select, text
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# Find a test user (use first active user)
|
||||
result = await db.execute(
|
||||
select(User).where(User.is_deleted == False).limit(1)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
print(" ⚠️ No active test user found - creating one...")
|
||||
# Create a minimal test scenario
|
||||
print(" ⚠️ Skipping DB soft delete test (no test user available)")
|
||||
return 0, 0
|
||||
|
||||
user_id = user.id
|
||||
old_email = user.email
|
||||
print(f" 📋 Test user: id={user_id}, email={old_email}")
|
||||
|
||||
# Check if Person exists
|
||||
person = None
|
||||
if user.person_id:
|
||||
result = await db.execute(
|
||||
select(Person).where(Person.id == user.person_id)
|
||||
)
|
||||
person = result.scalar_one_or_none()
|
||||
if person:
|
||||
print(f" 📋 Person record: id={person.id}, is_active={person.is_active}")
|
||||
|
||||
# Perform soft delete
|
||||
success = await AuthService.soft_delete_user(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
reason="test_script_validation",
|
||||
actor_id=user_id
|
||||
)
|
||||
|
||||
if not success:
|
||||
print(f" ❌ Soft delete returned False (user already deleted)")
|
||||
return 0, 1
|
||||
|
||||
# Verify user state
|
||||
await db.refresh(user)
|
||||
assert user.is_deleted == True, "is_deleted should be True"
|
||||
assert user.is_active == False, "is_active should be False"
|
||||
assert user.deleted_at is not None, "deleted_at should be set"
|
||||
assert "deleted_" in user.email, f"Email should be anonymized: {user.email}"
|
||||
assert str(user_id) in user.email, "Email should contain user_id"
|
||||
|
||||
print(f" ✅ User.is_deleted = {user.is_deleted}")
|
||||
print(f" ✅ User.is_active = {user.is_active}")
|
||||
print(f" ✅ User.deleted_at = {user.deleted_at}")
|
||||
print(f" ✅ User.email anonymized: {user.email}")
|
||||
|
||||
# Verify Person is UNTOUCHED by our soft delete
|
||||
if person:
|
||||
await db.refresh(person)
|
||||
# Person.deleted_at must remain None (our code never sets it)
|
||||
assert person.deleted_at is None, "Person.deleted_at should remain None (not touched by soft delete)"
|
||||
# Person data must be unchanged (no email rewrite, no name change)
|
||||
print(f" ✅ Person.deleted_at preserved = {person.deleted_at} (not touched)")
|
||||
print(f" ✅ Person data preserved (no modifications by soft delete)")
|
||||
else:
|
||||
print(f" ⚠️ No Person record linked to this user")
|
||||
|
||||
# Restore user for other tests
|
||||
user.is_deleted = False
|
||||
user.is_active = True
|
||||
user.deleted_at = None
|
||||
user.email = old_email
|
||||
await db.commit()
|
||||
print(f" ✅ User restored to original state")
|
||||
|
||||
return 2, 0
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
print(f" ❌ Soft delete test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 0, 1
|
||||
|
||||
p3, f3 = asyncio.run(test_soft_delete())
|
||||
|
||||
# ─── Final Summary ────────────────────────────────────────────────────────────
|
||||
print("\n" + "=" * 70)
|
||||
total_p = p1 + p2 + p3
|
||||
total_f = f1 + f2 + f3
|
||||
print(f"📊 FINAL RESULTS: {total_p} passed, {total_f} failed out of {total_p+total_f} tests")
|
||||
print("=" * 70)
|
||||
|
||||
if total_f > 0:
|
||||
print("❌ SOME TESTS FAILED!")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ ALL TESTS PASSED!")
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user