felhasználói felületre pü

This commit is contained in:
Roo
2026-07-27 08:39:18 +00:00
parent c9118bf52f
commit 6f28d3e70d
72 changed files with 6311 additions and 749 deletions

View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
User Relations Repair Script
-----------------------------
Idempotens: csak hiányzó rekordokat hoz létre.
Biztosítja, hogy minden aktív user rendelkezzen:
1. Personal Organization-nel (fleet.organizations) — ha még nincs
2. Wallet-tel (identity.wallets) — ha még nincs
3. Subscription-nel (finance.user_subscriptions, tier=private_free_v1) — ha még nincs
Futtatás:
docker compose exec sf_api python3 /app/backend/scripts/repair_user_relations.py
"""
import asyncio
import sys
import os
from datetime import datetime, timezone
from decimal import Decimal
from uuid import uuid4
# Path setup for the container
sys.path.insert(0, '/app')
sys.path.insert(0, '/app/backend')
os.environ.setdefault('DATABASE_URL', 'postgresql+asyncpg://...') # Will be read from .env
from app.database import AsyncSessionLocal
from sqlalchemy import select, text
from app.models.identity.identity import User, Wallet
from app.models.core_logic import UserSubscription
from app.models.marketplace.organization import Organization
from app.core.config import settings
DEFAULT_TIER_ID = 13 # private_free_v1
DEFAULT_CURRENCY = "HUF"
async def ensure_organization(db, user):
"""
Create personal organization if user doesn't have one.
Returns: org_id (existing or newly created)
"""
# Check if user already owns an organization (owner_id or legal_owner_id)
result = await db.execute(
select(Organization).where(
Organization.owner_id == user.id
).limit(1)
)
org = result.scalar_one_or_none()
if org:
print(f" [OK] Organization already exists for user {user.id}: org_id={org.id}")
return org.id
# Create new personal organization
folder_slug = f"user-{user.id}-{int(datetime.now(timezone.utc).timestamp())}"
now = datetime.now(timezone.utc)
org = Organization(
name=f"{user.email}'s Garage",
full_name=f"{user.email}'s Personal Garage",
display_name=f"{user.email}'s Garage",
folder_slug=folder_slug,
owner_id=user.id,
org_type="individual",
status="active",
is_active=True,
is_deleted=False,
is_verified=False,
subscription_plan="FREE",
base_asset_limit=5,
purchased_extra_slots=0,
notification_settings={},
external_integration_config={},
first_registered_at=now,
current_lifecycle_started_at=now,
lifecycle_index=1,
is_anonymized=False,
default_currency="HUF",
country_code="HU",
language="hu",
is_ownership_transferable=False,
visual_settings={},
is_affiliate_partner=False,
aliases=[],
tags=[],
settings={},
custom_features={},
created_at=now,
)
db.add(org)
await db.flush()
print(f" [CREATED] Organization for user {user.id}: org_id={org.id}")
return org.id
async def ensure_wallet(db, user, org_id):
"""
Create wallet if user doesn't have one.
wallet.user_id has UNIQUE constraint, so at most one wallet per user.
Returns: True if created, False if already existed
"""
result = await db.execute(
select(Wallet).where(Wallet.user_id == user.id).limit(1)
)
wallet = result.scalar_one_or_none()
if wallet:
print(f" [OK] Wallet already exists for user {user.id}: wallet_id={wallet.id}")
return False
wallet = Wallet(
user_id=user.id,
earned_credits=Decimal('0'),
purchased_credits=Decimal('0'),
service_coins=Decimal('0'),
currency=DEFAULT_CURRENCY,
organization_id=org_id,
)
db.add(wallet)
await db.flush()
print(f" [CREATED] Wallet for user {user.id}: wallet_id={wallet.id}")
return True
async def ensure_subscription(db, user):
"""
Create free tier subscription if user doesn't have one.
Returns: True if created, False if already existed
"""
result = await db.execute(
select(UserSubscription).where(UserSubscription.user_id == user.id).limit(1)
)
sub = result.scalar_one_or_none()
if sub:
print(f" [OK] Subscription already exists for user {user.id}: sub_id={sub.id}")
return False
sub = UserSubscription(
user_id=user.id,
tier_id=DEFAULT_TIER_ID,
valid_from=datetime.now(timezone.utc),
is_active=True,
)
db.add(sub)
await db.flush()
print(f" [CREATED] Subscription for user {user.id}: sub_id={sub.id}")
return True
async def repair_all_users():
"""Main repair loop."""
print("=" * 60)
print(" USER RELATIONS REPAIR SCRIPT")
print(f" Started at: {datetime.now(timezone.utc).isoformat()}")
print(f" Default tier: {DEFAULT_TIER_ID} (private_free_v1)")
print("=" * 60)
async with AsyncSessionLocal() as db:
# Get all active, non-deleted users
result = await db.execute(
select(User).where(
User.is_active == True,
User.is_deleted == False,
)
)
users = result.scalars().all()
stats = {
"total": len(users),
"wallets_created": 0,
"subs_created": 0,
"orgs_created": 0,
"errors": [],
}
print(f"\nFound {len(users)} active users to process.\n")
for user in users:
print(f"--- Processing user {user.id}: {user.email} ---")
try:
org_id = await ensure_organization(db, user)
# Track org creation — since ensure_organization always returns org_id,
# we can't easily diff "created" vs "existed" without checking first.
# We'll track it manually.
wallet_created = await ensure_wallet(db, user, org_id)
if wallet_created:
stats["wallets_created"] += 1
sub_created = await ensure_subscription(db, user)
if sub_created:
stats["subs_created"] += 1
except Exception as e:
err_msg = f"User {user.id} ({user.email}): {e}"
stats["errors"].append(err_msg)
print(f" [ERROR] {err_msg}")
await db.commit()
print("\n" + "=" * 60)
print(" REPAIR SUMMARY")
print("=" * 60)
print(f" Total active users processed: {stats['total']}")
print(f" Wallets created: {stats['wallets_created']}")
print(f" Subscriptions created: {stats['subs_created']}")
print(f" Errors: {len(stats['errors'])}")
if stats["errors"]:
print("\n Errors:")
for err in stats["errors"]:
print(f" - {err}")
print("=" * 60)
return stats
if __name__ == "__main__":
asyncio.run(repair_all_users())

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
Seed script: Populate audit.financial_ledger with 20 dummy records.
Uses psycopg2 (sync) to bypass asyncpg prepared statement enum validation issues.
"""
import uuid
import random
import os
from datetime import datetime, timedelta, timezone
import psycopg2
TRANSACTION_TYPES = [
"SUBSCRIPTION", "COMMISSION", "REFUND", "MANUAL_ADJUSTMENT",
"PURCHASE", "WITHDRAWAL", "BONUS", "FEE", "REWARD",
]
DB_ENTRY_TYPES = ["CREDIT", "DEBIT"]
DB_WALLET_TYPES = ["EARNED", "PURCHASED", "SERVICE_COINS", "VOUCHER"]
DB_STATUSES = ["PENDING", "SUCCESS", "FAILED", "REFUNDED"]
TEST_USER_IDS = [1, 2, 28, 29, 55, 75, 79, 85, 86, 88, 100, 102, 103, 104, 105, 106]
NOW = datetime.now(timezone.utc)
def get_dsn_params():
"""Extract connection parameters from DATABASE_URL."""
db_url = os.environ.get("DATABASE_URL", "postgresql+asyncpg://service_finder_app:AppSafePass_2026@shared-postgres:5432/service_finder")
db_url = db_url.replace("postgresql+asyncpg://", "postgresql://")
# Parse URL
parts = db_url.replace("postgresql://", "").split("@")
user_pass = parts[0].split(":")
host_db = parts[1].split("/")
host_port = host_db[0].split(":")
return {
"user": user_pass[0],
"password": user_pass[1],
"host": host_port[0],
"port": int(host_port[1]) if len(host_port) > 1 else 5432,
"dbname": host_db[1].split("?")[0],
}
def seed():
params = get_dsn_params()
conn = psycopg2.connect(**params)
conn.autocommit = False
try:
with conn.cursor() as cur:
# Count existing
cur.execute("SELECT COUNT(*) FROM audit.financial_ledger")
print(f"Existing records: {cur.fetchone()[0]}")
# Get person_ids
person_map = {}
for uid in TEST_USER_IDS:
cur.execute("SELECT person_id FROM identity.users WHERE id = %s", (uid,))
pid = cur.fetchone()
if pid and pid[0]:
person_map[uid] = pid[0]
if not person_map:
print("No users with person_id found!")
return
print(f"Users with person records: {list(person_map.keys())}")
inserted = 0
for i in range(20):
uid = random.choice(list(person_map.keys()))
pid = person_map[uid]
entry_t = random.choice(DB_ENTRY_TYPES)
wallet_t = random.choice(DB_WALLET_TYPES)
stat = random.choice(DB_STATUSES)
txn_t = random.choice(TRANSACTION_TYPES)
cur_r = random.choice(["HUF", "EUR", "USD", "GBP"])
amt = round(random.uniform(5.0, 5000.0), 2)
days_ago = random.randint(0, 30)
created_ts = NOW - timedelta(
days=random.randint(0, days_ago),
hours=random.randint(0, 23),
minutes=random.randint(0, 59),
)
ref = "TXN-" + uuid.uuid4().hex[:8].upper()
desc = txn_t.lower().replace("_", " ").title() + f" for user {uid}"
net_amt = round(amt / 1.27, 2) if random.random() > 0.3 else None
tax_amt = round(amt - net_amt, 2) if net_amt else None
balance = round(random.uniform(100, 50000), 2)
inv_stat = "PAID" if stat == "completed" else stat.upper()
txn_id = str(uuid.uuid4())
# psycopg2 handles text → enum cast correctly with simple execute
cur.execute(
"""
INSERT INTO audit.financial_ledger
(user_id, person_id, amount, currency, transaction_type, details,
created_at, entry_type, balance_after, wallet_type, status,
transaction_id, net_amount, tax_amount, gross_amount, invoice_status)
VALUES (
%s, %s, %s, %s, %s,
%s::jsonb,
%s::timestamptz,
%s::audit.ledger_entry_type,
%s,
%s::audit.wallet_type,
%s::audit.ledger_status,
%s::uuid,
%s, %s, %s, %s
)
""",
(
uid, pid, amt, cur_r, txn_t,
f'{{"description": "{desc}", "reference": "{ref}"}}',
created_ts.isoformat(),
entry_t, balance, wallet_t, stat, txn_id,
net_amt, tax_amt, amt, inv_stat,
),
)
inserted += 1
conn.commit()
print(f"✅ Inserted {inserted} records successfully!")
cur.execute("SELECT COUNT(*) FROM audit.financial_ledger")
print(f"Total records now: {cur.fetchone()[0]}")
cur.execute("""
SELECT
fl.entry_type::text,
fl.wallet_type::text,
fl.status::text,
COUNT(*)::int,
ROUND(AVG(fl.amount)::numeric, 2),
u.email
FROM audit.financial_ledger fl
LEFT JOIN identity.users u ON u.id = fl.user_id
GROUP BY fl.entry_type, fl.wallet_type, fl.status, u.email
ORDER BY fl.entry_type, fl.wallet_type
""")
rows = cur.fetchall()
total_sum = sum(r[3] for r in rows) if rows else 0
print(f"\n📊 Summary (total {total_sum} records):")
print(f" {'Entry':8} {'Wallet':15} {'Status':10} {'Cnt':5} {'Avg Amt':10} {'User'}")
print(f" {'-'*8} {'-'*15} {'-'*10} {'-'*5} {'-'*10} {'-'*30}")
for r in rows:
print(f" {r[0]:8} {r[1]:15} {r[2]:10} {r[3]:5} {r[4]:10} {r[5] or 'N/A':30}")
finally:
conn.close()
if __name__ == "__main__":
seed()