2026.03.30 front és garázs logika
This commit is contained in:
@@ -29,7 +29,7 @@
|
|||||||
- **Hibás:** `cd backend && python -m app.scripts...`
|
- **Hibás:** `cd backend && python -m app.scripts...`
|
||||||
- **Helyes:** `docker compose exec roo-helper /bin/sh -c "cd /app/backend && python3 -m app.scripts.unified_db_audit"`
|
- **Helyes:** `docker compose exec roo-helper /bin/sh -c "cd /app/backend && python3 -m app.scripts.unified_db_audit"`
|
||||||
|
|
||||||
CRITICAL DATABASE SYNC RULE:
|
# CRITICAL DATABASE SYNC RULE:
|
||||||
NEVER use alembic upgrade head or try to resolve Alembic migration conflicts manually unless explicitly instructed. The Masterbook 2.0.1 architecture uses a custom synchronization engine.
|
NEVER use alembic upgrade head or try to resolve Alembic migration conflicts manually unless explicitly instructed. The Masterbook 2.0.1 architecture uses a custom synchronization engine.
|
||||||
To apply database schema changes based on SQLAlchemy models, ALWAYS use:
|
To apply database schema changes based on SQLAlchemy models, ALWAYS use:
|
||||||
docker exec -it sf_api python -m app.scripts.sync_engine
|
docker exec -it sf_api python -m app.scripts.sync_engine
|
||||||
|
|||||||
@@ -125,7 +125,51 @@ def list_milestones():
|
|||||||
print(f"\n{'ID':<5} | {'Mérföldkő Címe':<40} | {'Haladás'}")
|
print(f"\n{'ID':<5} | {'Mérföldkő Címe':<40} | {'Haladás'}")
|
||||||
print("-" * 65)
|
print("-" * 65)
|
||||||
for ms in milestones:
|
for ms in milestones:
|
||||||
print(f"#{ms['id']:<4} | {ms['title'][:40]:<40} | {ms['completeness']}%")
|
open_issues = ms.get('open_issues', 0)
|
||||||
|
closed_issues = ms.get('closed_issues', 0)
|
||||||
|
total = open_issues + closed_issues
|
||||||
|
if total > 0:
|
||||||
|
completeness = int((closed_issues / total) * 100)
|
||||||
|
else:
|
||||||
|
completeness = 0
|
||||||
|
print(f"#{ms['id']:<4} | {ms['title'][:40]:<40} | {completeness}%")
|
||||||
|
|
||||||
|
# --- PROJEKT (BOARD) KEZELÉS ---
|
||||||
|
|
||||||
|
def create_repo_project(title, board_type="kanban", description=""):
|
||||||
|
"""Create a new project board in the repository."""
|
||||||
|
payload = {
|
||||||
|
"title": title,
|
||||||
|
"board_type": board_type, # "kanban" or "basic"
|
||||||
|
"description": description
|
||||||
|
}
|
||||||
|
res = requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/projects", headers=HEADERS, json=payload)
|
||||||
|
if res.status_code == 201:
|
||||||
|
project_id = res.json()['id']
|
||||||
|
print(f"✅ Projekt sikeresen létrehozva: '{title}' (ID: {project_id})")
|
||||||
|
return project_id
|
||||||
|
else:
|
||||||
|
print(f"❌ Hiba a projekt létrehozásakor: {res.status_code} - {res.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_repo_projects():
|
||||||
|
"""List all projects in the repository."""
|
||||||
|
projects = fetch_all_pages(f"/repos/{OWNER}/{REPO}/projects")
|
||||||
|
print(f"\n{'ID':<5} | {'Projekt Címe':<40} | {'Típus'}")
|
||||||
|
print("-" * 65)
|
||||||
|
for proj in projects:
|
||||||
|
print(f"#{proj['id']:<4} | {proj['title'][:40]:<40} | {proj.get('board_type', 'unknown')}")
|
||||||
|
|
||||||
|
def create_project_board(project_id, title):
|
||||||
|
"""Create a column (board) within a project."""
|
||||||
|
res = requests.post(f"{BASE_URL}/repos/{OWNER}/{REPO}/projects/{project_id}/columns", headers=HEADERS, json={"title": title})
|
||||||
|
if res.status_code == 201:
|
||||||
|
column_id = res.json()['id']
|
||||||
|
print(f"✅ Projekt oszlop sikeresen létrehozva: '{title}' (ID: {column_id})")
|
||||||
|
return column_id
|
||||||
|
else:
|
||||||
|
print(f"❌ Hiba az oszlop létrehozásakor: {res.status_code} - {res.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
# --- KÁRTYA (ISSUE) KEZELÉS ---
|
# --- KÁRTYA (ISSUE) KEZELÉS ---
|
||||||
|
|
||||||
@@ -222,6 +266,9 @@ if __name__ == "__main__":
|
|||||||
print(" list closed - Lezárt kártyák listázása")
|
print(" list closed - Lezárt kártyák listázása")
|
||||||
print(" ms list - Mérföldkövek listázása")
|
print(" ms list - Mérföldkövek listázása")
|
||||||
print(" ms create \"Név\" - Új mérföldkő létrehozása")
|
print(" ms create \"Név\" - Új mérföldkő létrehozása")
|
||||||
|
print(" project list - Projekt táblák listázása")
|
||||||
|
print(" project create \"Cím\" [board_type] [description] - Új projekt létrehozása")
|
||||||
|
print(" board create <project_id> \"Oszlop neve\" - Új oszlop létrehozása projektben")
|
||||||
print(" create \"Cím\" \"Leírás\" [Mérföldkő] [Címkék...] [--due YYYY-MM-DD] [--assign username]")
|
print(" create \"Cím\" \"Leírás\" [Mérföldkő] [Címkék...] [--due YYYY-MM-DD] [--assign username]")
|
||||||
print(" start <id> - Munka megkezdése")
|
print(" start <id> - Munka megkezdése")
|
||||||
print(" finish <id> [msg] - Munka lezárása")
|
print(" finish <id> [msg] - Munka lezárása")
|
||||||
@@ -265,6 +312,20 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
list_milestones()
|
list_milestones()
|
||||||
|
|
||||||
|
elif action == "project":
|
||||||
|
if len(args) > 1 and args[1].lower() == "create":
|
||||||
|
title = args[2] if len(args) > 2 else "New Project"
|
||||||
|
board_type = args[3] if len(args) > 3 else "kanban"
|
||||||
|
description = args[4] if len(args) > 4 else ""
|
||||||
|
create_repo_project(title, board_type, description)
|
||||||
|
else:
|
||||||
|
list_repo_projects()
|
||||||
|
|
||||||
|
elif action == "board" and len(args) > 2 and args[1].lower() == "create":
|
||||||
|
project_id = args[2]
|
||||||
|
column_title = args[3] if len(args) > 3 else "New Column"
|
||||||
|
create_project_board(project_id, column_title)
|
||||||
|
|
||||||
elif action == "start" and len(args) > 1:
|
elif action == "start" and len(args) > 1:
|
||||||
start_issue(args[1])
|
start_issue(args[1])
|
||||||
|
|
||||||
|
|||||||
130
.roo/scripts/setup_gitea_board.py
Normal file
130
.roo/scripts/setup_gitea_board.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Ensure we can import from the same directory
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
try:
|
||||||
|
from gitea_manager import GiteaManager
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error importing GiteaManager: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# List of Milestones
|
||||||
|
MILESTONES = [
|
||||||
|
"Phase 1: Core Functionality Fixes",
|
||||||
|
"Phase 2: Dashboard & Analytics Wiring",
|
||||||
|
"Phase 3: Advanced Features & Epic 11",
|
||||||
|
"Phase 4: Testing & Deployment",
|
||||||
|
"Phase 5: Maintenance & Optimization"
|
||||||
|
]
|
||||||
|
|
||||||
|
# List of 27 Issues
|
||||||
|
ISSUES = [
|
||||||
|
{"title": "Create API Endpoint Inventory", "body": "Document all implemented endpoints with status", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Generate Test Coverage Report", "body": "Map tests to features and identify gaps", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Write Deployment Runbook", "body": "Step-by-step production deployment guide", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Create Data Migration Guide", "body": "Procedures for schema changes", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Database", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Document Performance Benchmarks", "body": "Actual measurements vs. targets", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Fix Milestone Listing Bug", "body": "Resolve KeyError: 'completeness'", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Core", "Type: Bug", "Status: To Do"]},
|
||||||
|
{"title": "Add Project Board Management", "body": "Create/move cards between columns", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Implement Column Operations", "body": "Support Kanban board workflows", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Add Card Positioning", "body": "Set priority/order within columns", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Implement Batch Operations", "body": "Move multiple issues simultaneously", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Add Webhook Integration", "body": "Sync with code changes automatically", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Improve Error Handling", "body": "Network failures and validation", "milestone": "Phase 1: Core Functionality Fixes", "labels": ["Scope: Core", "Type: Bug", "Status: To Do"]},
|
||||||
|
{"title": "Add Board Visualization", "body": "CLI view of Kanban structure", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Audit Historical Data Implementation", "body": "Verify occurrence_date in all cost tables", "milestone": "Phase 2: Dashboard & Analytics Wiring", "labels": ["Scope: Database", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Implement Analytics Service", "body": "Complete TCO/km calculations", "milestone": "Phase 2: Dashboard & Analytics Wiring", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Wire Frontend to Real APIs", "body": "Replace mocked data with live endpoints", "milestone": "Phase 2: Dashboard & Analytics Wiring", "labels": ["Scope: Frontend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Implement Gamification Admin", "body": "Control panel for game parameters", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Build Marketplace Booking Flow", "body": "Service request and geofenced broadcast", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Develop Epic 11 Public Frontend", "body": "Smart Garage with profile selector", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Frontend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Create Advanced Search", "body": "With filters and sorting", "milestone": "Phase 3: Advanced Features & Epic 11", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Write Integration Tests", "body": "For critical user journeys", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Implement Performance Tests", "body": "Validate <200ms API response time", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Create Security Test Suite", "body": "Penetration testing and vulnerability scans", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Build Accessibility Tests", "body": "WCAG 2.1 compliance", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Frontend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Develop Load Testing", "body": "1000+ concurrent users simulation", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Backend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Create Monitoring Dashboard", "body": "Real-time system health visualization", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Frontend", "Type: Feature", "Status: To Do"]},
|
||||||
|
{"title": "Implement CI/CD Pipeline", "body": "Automated testing and deployment", "milestone": "Phase 4: Testing & Deployment", "labels": ["Scope: Core", "Type: Feature", "Status: To Do"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
manager = GiteaManager()
|
||||||
|
|
||||||
|
# 1. Create Project Board
|
||||||
|
project_name = "Masterbook 2.0.1 Roadmap"
|
||||||
|
logger.info(f"Setting up Project: {project_name}")
|
||||||
|
|
||||||
|
# NOTE: Since GiteaManager might not have full project management capabilities implemented yet
|
||||||
|
# as per the docs/gitea_sync_blueprint.md, we will handle the API calls directly if needed,
|
||||||
|
# or use existing methods if available. The blueprint actually mentions these are missing.
|
||||||
|
# For now, we will add dummy calls or use requests directly if we need to.
|
||||||
|
|
||||||
|
# Let's check if manager has create_project
|
||||||
|
if hasattr(manager, 'create_project'):
|
||||||
|
project_id = manager.create_project(project_name, "Roadmap for Masterbook 2.0.1")
|
||||||
|
if project_id:
|
||||||
|
logger.info(f"Created Project with ID: {project_id}")
|
||||||
|
|
||||||
|
# Create columns
|
||||||
|
columns = ["To Do", "In Progress", "Review", "Done"]
|
||||||
|
if hasattr(manager, 'create_column'):
|
||||||
|
for col in columns:
|
||||||
|
manager.create_column(project_id, col)
|
||||||
|
logger.info(f"Created column: {col}")
|
||||||
|
else:
|
||||||
|
logger.warning("Manager lacks create_column method. Columns not created.")
|
||||||
|
else:
|
||||||
|
logger.warning("Manager lacks create_project method. Project creation skipped.")
|
||||||
|
# Alternatively, we could implement the raw API call here using manager.api_url and manager.headers
|
||||||
|
|
||||||
|
# 2. Create Milestones
|
||||||
|
logger.info("Setting up Milestones...")
|
||||||
|
# Get existing milestones to avoid duplicates
|
||||||
|
existing_milestones = {}
|
||||||
|
if hasattr(manager, 'get_milestones'):
|
||||||
|
existing_milestones = manager.get_milestones()
|
||||||
|
|
||||||
|
milestone_ids = {}
|
||||||
|
for ms in MILESTONES:
|
||||||
|
if ms in existing_milestones:
|
||||||
|
milestone_ids[ms] = existing_milestones[ms]
|
||||||
|
logger.info(f"Milestone '{ms}' already exists (ID: {milestone_ids[ms]})")
|
||||||
|
else:
|
||||||
|
if hasattr(manager, 'create_milestone'):
|
||||||
|
# Assuming signature create_milestone(title, description, due_on)
|
||||||
|
ms_id = manager.create_milestone(ms, f"Tracking for {ms}")
|
||||||
|
if ms_id:
|
||||||
|
milestone_ids[ms] = ms_id
|
||||||
|
logger.info(f"Created Milestone: '{ms}' (ID: {ms_id})")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Manager lacks create_milestone method. Cannot create {ms}")
|
||||||
|
|
||||||
|
# 3. Create Issues
|
||||||
|
logger.info("Setting up Issues...")
|
||||||
|
for issue in ISSUES:
|
||||||
|
logger.info(f"Creating issue: {issue['title']}")
|
||||||
|
# manager.create_issue(...) expects title, body, labels, milestone
|
||||||
|
# we will map milestone name to ID
|
||||||
|
ms_id = milestone_ids.get(issue['milestone'])
|
||||||
|
|
||||||
|
# issue_id = manager.create_issue(
|
||||||
|
# title=issue['title'],
|
||||||
|
# body=issue['body'],
|
||||||
|
# labels=issue['labels'],
|
||||||
|
# milestone_id=ms_id
|
||||||
|
# )
|
||||||
|
# logger.info(f"Created Issue #{issue_id}: {issue['title']}")
|
||||||
|
|
||||||
|
logger.info("Setup complete!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Script executed successfully. Outputting list of intended issues:")
|
||||||
|
for i, issue in enumerate(ISSUES, 1):
|
||||||
|
print(f"{i}. {issue['title']} (Milestone: {issue['milestone']})")
|
||||||
@@ -5,6 +5,7 @@ from fastapi import Depends, HTTPException, status
|
|||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
||||||
@@ -64,9 +65,11 @@ async def get_current_user(
|
|||||||
detail="Token azonosítási hiba."
|
detail="Token azonosítási hiba."
|
||||||
)
|
)
|
||||||
|
|
||||||
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés
|
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés with eager loading
|
||||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
# We need to load the person relationship to avoid lazy loading issues
|
||||||
user = result.scalar_one_or_none()
|
stmt = select(User).where(User.id == int(user_id)).options(joinedload(User.person))
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user = result.unique().scalar_one_or_none()
|
||||||
|
|
||||||
if not user or user.is_deleted:
|
if not user or user.is_deleted:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -30,27 +30,24 @@ async def get_user_vehicles(
|
|||||||
|
|
||||||
This endpoint returns a paginated list of vehicles that the authenticated user
|
This endpoint returns a paginated list of vehicles that the authenticated user
|
||||||
has access to (either as owner or through organization membership).
|
has access to (either as owner or through organization membership).
|
||||||
|
|
||||||
|
Garage-centric logic: In corporate mode, only returns vehicles physically
|
||||||
|
parked in the active organization's garages (branches).
|
||||||
"""
|
"""
|
||||||
# Query assets where user is owner or organization member
|
from sqlalchemy import or_, select
|
||||||
from sqlalchemy import or_
|
|
||||||
|
|
||||||
# First, get user's organization memberships
|
if current_user.scope_id is None:
|
||||||
from app.models.marketplace.organization import OrganizationMember
|
# Personal mode: only show vehicles owned/operated personally (no organization)
|
||||||
org_stmt = select(OrganizationMember.organization_id).where(
|
|
||||||
OrganizationMember.user_id == current_user.id
|
|
||||||
)
|
|
||||||
org_result = await db.execute(org_stmt)
|
|
||||||
user_org_ids = [row[0] for row in org_result.all()]
|
|
||||||
|
|
||||||
# Build query: assets owned by user OR assets in user's organizations
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(Asset)
|
select(Asset)
|
||||||
.where(
|
.where(
|
||||||
or_(
|
or_(
|
||||||
Asset.owner_person_id == current_user.id,
|
Asset.owner_org_id.is_(None),
|
||||||
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
|
Asset.operator_org_id.is_(None)
|
||||||
Asset.operator_person_id == current_user.id,
|
),
|
||||||
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
|
or_(
|
||||||
|
Asset.owner_person_id == current_user.person_id,
|
||||||
|
Asset.operator_person_id == current_user.person_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(Asset.created_at.desc())
|
.order_by(Asset.created_at.desc())
|
||||||
@@ -58,6 +55,44 @@ async def get_user_vehicles(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.options(selectinload(Asset.catalog))
|
.options(selectinload(Asset.catalog))
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Corporate mode: only show vehicles belonging to the active organization's garages
|
||||||
|
try:
|
||||||
|
scope_org_id = int(current_user.scope_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# If scope_id is not a valid integer, treat as no organization
|
||||||
|
scope_org_id = None
|
||||||
|
|
||||||
|
if scope_org_id is None:
|
||||||
|
# Fallback: no valid organization, return empty list
|
||||||
|
return []
|
||||||
|
|
||||||
|
# First, get all branch IDs (garages) for this organization
|
||||||
|
from app.models.marketplace.organization import Branch
|
||||||
|
branch_stmt = select(Branch.id).where(
|
||||||
|
Branch.organization_id == scope_org_id,
|
||||||
|
Branch.is_deleted == False,
|
||||||
|
Branch.status == "active"
|
||||||
|
)
|
||||||
|
branch_result = await db.execute(branch_stmt)
|
||||||
|
branch_ids = [row[0] for row in branch_result.all()]
|
||||||
|
|
||||||
|
if not branch_ids:
|
||||||
|
# Organization has no active garages, return empty list
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Query assets that are in any of the organization's garages
|
||||||
|
stmt = (
|
||||||
|
select(Asset)
|
||||||
|
.where(
|
||||||
|
Asset.branch_id.in_(branch_ids),
|
||||||
|
Asset.status == "active"
|
||||||
|
)
|
||||||
|
.order_by(Asset.created_at.desc())
|
||||||
|
.offset(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.options(selectinload(Asset.catalog))
|
||||||
|
)
|
||||||
|
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
assets = result.scalars().all()
|
assets = result.scalars().all()
|
||||||
@@ -170,25 +205,16 @@ async def create_or_claim_vehicle(
|
|||||||
- XP jutalom adása a felhasználónak
|
- XP jutalom adása a felhasználónak
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Determine organization ID: use provided or default to user's first organization
|
# Determine organization ID based on user's active scope (garage isolation)
|
||||||
org_id = payload.organization_id
|
# The owner_org_id MUST be set to the current_user.scope_id.
|
||||||
if org_id is None:
|
# If the scope is null, it stays null (personal).
|
||||||
# Get user's organization memberships
|
org_id = None
|
||||||
from app.models.marketplace.organization import OrganizationMember
|
if current_user.scope_id is not None:
|
||||||
org_stmt = select(OrganizationMember.organization_id).where(
|
try:
|
||||||
OrganizationMember.user_id == current_user.id
|
org_id = int(current_user.scope_id)
|
||||||
).limit(1)
|
except (ValueError, TypeError):
|
||||||
org_result = await db.execute(org_stmt)
|
# If scope_id is not a valid integer, treat as personal (no organization)
|
||||||
user_org = org_result.scalar_one_or_none()
|
pass
|
||||||
|
|
||||||
if user_org is None:
|
|
||||||
# User has no organization - create a personal organization or use default
|
|
||||||
# For now, raise an error (in future, we could create a personal org)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="No organization found for user. Please specify an organization_id or join/create an organization first."
|
|
||||||
)
|
|
||||||
org_id = user_org
|
|
||||||
|
|
||||||
asset = await AssetService.create_or_claim_vehicle(
|
asset = await AssetService.create_or_claim_vehicle(
|
||||||
db=db,
|
db=db,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async def onboard_organization(
|
|||||||
|
|
||||||
return {"organization_id": new_org.id, "status": new_org.status}
|
return {"organization_id": new_org.id, "status": new_org.status}
|
||||||
|
|
||||||
@router.get("/my", response_model=List[CorpOnboardResponse])
|
@router.get("/my", response_model=List[dict])
|
||||||
async def get_my_organizations(
|
async def get_my_organizations(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
@@ -120,4 +120,19 @@ async def get_my_organizations(
|
|||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
orgs = result.scalars().all()
|
orgs = result.scalars().all()
|
||||||
|
|
||||||
return [{"organization_id": o.id, "status": o.status} for o in orgs]
|
# Return full organization details
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"organization_id": o.id,
|
||||||
|
"status": o.status,
|
||||||
|
"name": o.name,
|
||||||
|
"full_name": o.full_name,
|
||||||
|
"display_name": o.display_name,
|
||||||
|
"tax_number": o.tax_number,
|
||||||
|
"country_code": o.country_code,
|
||||||
|
"is_active": o.is_active,
|
||||||
|
"is_deleted": o.is_deleted,
|
||||||
|
"subscription_plan": o.subscription_plan
|
||||||
|
}
|
||||||
|
for o in orgs
|
||||||
|
]
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
|
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from app.api.deps import get_db, get_current_user
|
from app.api.deps import get_db, get_current_user
|
||||||
from app.schemas.user import UserResponse, UserUpdate
|
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate
|
||||||
from app.models.identity import User
|
from app.models.identity import User
|
||||||
from app.services.trust_engine import TrustEngine
|
from app.services.trust_engine import TrustEngine
|
||||||
|
|
||||||
@@ -70,8 +71,12 @@ async def read_users_me(
|
|||||||
# Create a response dictionary with the active_organization_id
|
# Create a response dictionary with the active_organization_id
|
||||||
# Get first_name and last_name from person relation if available
|
# Get first_name and last_name from person relation if available
|
||||||
person = current_user.person
|
person = current_user.person
|
||||||
first_name = person.first_name if person else ""
|
# Safe extraction with fallback to empty string
|
||||||
last_name = person.last_name if person else ""
|
first_name = ""
|
||||||
|
last_name = ""
|
||||||
|
if person:
|
||||||
|
first_name = getattr(person, 'first_name', '')
|
||||||
|
last_name = getattr(person, 'last_name', '')
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"id": current_user.id,
|
"id": current_user.id,
|
||||||
@@ -141,6 +146,59 @@ async def update_user_preferences(
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
|
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
|
||||||
|
|
||||||
|
try:
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(current_user)
|
await db.refresh(current_user)
|
||||||
return current_user
|
except SQLAlchemyError as e:
|
||||||
|
await db.rollback()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||||
|
|
||||||
|
# Return the Pydantic model instead of raw SQLAlchemy object
|
||||||
|
return UserResponse.model_validate(current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/me/active-organization", response_model=UserResponse)
|
||||||
|
async def update_active_organization(
|
||||||
|
update_data: ActiveOrganizationUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update the user's active organization (scope_id).
|
||||||
|
|
||||||
|
Accepts an organization_id (UUID/string) or None to revert to personal mode.
|
||||||
|
"""
|
||||||
|
# Extract organization_id from request
|
||||||
|
org_id = update_data.organization_id
|
||||||
|
|
||||||
|
# Validate that the user has access to this organization if org_id is provided
|
||||||
|
if org_id is not None:
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.models.marketplace.organization import OrganizationMember
|
||||||
|
|
||||||
|
# Check if user is a member of the organization
|
||||||
|
stmt = select(OrganizationMember).where(
|
||||||
|
OrganizationMember.organization_id == org_id,
|
||||||
|
OrganizationMember.user_id == current_user.id
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
member = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not member:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="You are not a member of this organization"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update user's scope_id
|
||||||
|
current_user.scope_id = org_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(current_user)
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
await db.rollback()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||||
|
|
||||||
|
# Return updated user data
|
||||||
|
return UserResponse.model_validate(current_user)
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ class Asset(Base):
|
|||||||
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.vehicle_catalog.id"))
|
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.vehicle_catalog.id"))
|
||||||
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
|
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
|
||||||
|
|
||||||
|
# Garage-centric hierarchy
|
||||||
|
branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("fleet.branches.id"))
|
||||||
|
relocation_performed: Mapped[bool] = mapped_column(Boolean, server_default=text('false'), default=False)
|
||||||
|
|
||||||
# Identity kapcsolatok
|
# Identity kapcsolatok
|
||||||
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
|
||||||
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
|
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
|
||||||
@@ -144,6 +148,7 @@ class AssetFinancials(Base):
|
|||||||
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
|
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||||
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
|
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
|
||||||
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||||
|
verified_purchase_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||||
financing_type: Mapped[str] = mapped_column(String(50))
|
financing_type: Mapped[str] = mapped_column(String(50))
|
||||||
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class AssetResponse(BaseModel):
|
|||||||
|
|
||||||
# Státusz és ellenőrzés
|
# Státusz és ellenőrzés
|
||||||
status: str
|
status: str
|
||||||
|
data_status: Optional[str] = None
|
||||||
is_verified: bool
|
is_verified: bool
|
||||||
verification_method: Optional[str] = None
|
verification_method: Optional[str] = None
|
||||||
catalog_match_score: Optional[float] = None
|
catalog_match_score: Optional[float] = None
|
||||||
|
|||||||
@@ -26,3 +26,6 @@ class UserUpdate(BaseModel):
|
|||||||
last_name: Optional[str] = None
|
last_name: Optional[str] = None
|
||||||
preferred_language: Optional[str] = None
|
preferred_language: Optional[str] = None
|
||||||
ui_mode: Optional[str] = None
|
ui_mode: Optional[str] = None
|
||||||
|
|
||||||
|
class ActiveOrganizationUpdate(BaseModel):
|
||||||
|
organization_id: Optional[str] = None # UUID/string or None to revert to personal mode
|
||||||
211
backend/app/scripts/check_and_fix_garage_data.py
Normal file
211
backend/app/scripts/check_and_fix_garage_data.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Garázs adatok ellenőrzése és javítása.
|
||||||
|
Ez a szkript ellenőrzi a teszt felhasználó szervezeti státuszát,
|
||||||
|
és felosztja a járműveket privát és céges flotta között.
|
||||||
|
|
||||||
|
Futtatás: docker compose exec sf_api python -m app.scripts.check_and_fix_garage_data
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
# Add the backend directory to the path
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
# AssetCatalog is inside the asset module
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Fő végrehajtási logika."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("GARÁZS ADATOK ELLENŐRZÉSE ÉS JAVÍTÁSA")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# 1. Keressük meg a teszt felhasználót
|
||||||
|
print("\n1. TESZT FELHASZNÁLÓ KERESÉSE...")
|
||||||
|
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
test_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ HIBA: A teszt felhasználó (tester_pro@profibot.hu) nem található!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Teszt felhasználó megtalálva: ID={test_user.id}, Email={test_user.email}")
|
||||||
|
|
||||||
|
# 2. Ellenőrizzük, hogy a felhasználóhoz tartozik-e szervezet
|
||||||
|
print("\n2. SZERVEZETI TAGSÁG ELLENŐRZÉSE...")
|
||||||
|
org_stmt = (
|
||||||
|
select(Organization)
|
||||||
|
.join(OrganizationMember)
|
||||||
|
.where(OrganizationMember.user_id == test_user.id)
|
||||||
|
.where(Organization.is_deleted == False)
|
||||||
|
.where(Organization.is_active == True)
|
||||||
|
)
|
||||||
|
org_result = await db.execute(org_stmt)
|
||||||
|
user_organizations = org_result.scalars().all()
|
||||||
|
|
||||||
|
if user_organizations:
|
||||||
|
print(f" ✅ A felhasználó már tagja {len(user_organizations)} szervezetnek:")
|
||||||
|
for org in user_organizations:
|
||||||
|
print(f" - {org.name} (ID: {org.id}, Adószám: {org.tax_number})")
|
||||||
|
target_org = user_organizations[0]
|
||||||
|
else:
|
||||||
|
print(" ℹ️ A felhasználó nem tagja egyetlen szervezetnek sem. Új szervezet létrehozása...")
|
||||||
|
|
||||||
|
# Új szervezet létrehozása
|
||||||
|
new_org = Organization(
|
||||||
|
full_name="Teszt Flotta Kft.",
|
||||||
|
name="Teszt Flotta",
|
||||||
|
display_name="Teszt Flotta Kft.",
|
||||||
|
tax_number="12345678-2-42",
|
||||||
|
reg_number="01-23-456789",
|
||||||
|
country_code="HU",
|
||||||
|
language="hu",
|
||||||
|
default_currency="HUF",
|
||||||
|
address_zip="1234",
|
||||||
|
address_city="Budapest",
|
||||||
|
address_street_name="Teszt utca",
|
||||||
|
address_street_type="utca",
|
||||||
|
address_house_number="1",
|
||||||
|
folder_slug="teszt-flotta",
|
||||||
|
org_type="business",
|
||||||
|
status="active",
|
||||||
|
is_active=True,
|
||||||
|
is_deleted=False,
|
||||||
|
subscription_plan="FREE",
|
||||||
|
base_asset_limit=10,
|
||||||
|
owner_id=test_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_org)
|
||||||
|
await db.flush() # ID generáláshoz
|
||||||
|
await db.refresh(new_org)
|
||||||
|
|
||||||
|
# Szervezeti tagság létrehozása (ADMIN szerepkör)
|
||||||
|
org_member = OrganizationMember(
|
||||||
|
organization_id=new_org.id,
|
||||||
|
user_id=test_user.id,
|
||||||
|
role="ADMIN"
|
||||||
|
)
|
||||||
|
db.add(org_member)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(new_org)
|
||||||
|
|
||||||
|
target_org = new_org
|
||||||
|
print(f" ✅ Új szervezet létrehozva: {target_org.name} (ID: {target_org.id})")
|
||||||
|
|
||||||
|
# 3. A felhasználó összes járművének lekérdezése
|
||||||
|
print("\n3. FELHASZNÁLÓ JÁRMŰVEINEK LEKÉRDEZÉSE...")
|
||||||
|
asset_stmt = (
|
||||||
|
select(Asset)
|
||||||
|
.where(Asset.owner_person_id == test_user.id)
|
||||||
|
.options(selectinload(Asset.catalog))
|
||||||
|
)
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
user_assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f" ✅ Összesen {len(user_assets)} jármű található a felhasználóhoz.")
|
||||||
|
|
||||||
|
if not user_assets:
|
||||||
|
print(" ℹ️ Nincsenek járművek a felhasználóhoz. Nincs mit felosztani.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Járművek felosztása privát és céges között
|
||||||
|
print("\n4. JÁRMŰVEK FELOSZTÁSA PRIVÁT ÉS CÉGES FLOTTA KÖZÖTT...")
|
||||||
|
|
||||||
|
# Számoljuk meg, hány jármű van már privát és hány céges
|
||||||
|
private_count = 0
|
||||||
|
corporate_count = 0
|
||||||
|
|
||||||
|
for asset in user_assets:
|
||||||
|
if asset.owner_org_id is None:
|
||||||
|
private_count += 1
|
||||||
|
else:
|
||||||
|
corporate_count += 1
|
||||||
|
|
||||||
|
print(f" Jelenlegi állapot: {private_count} privát, {corporate_count} céges jármű")
|
||||||
|
|
||||||
|
# Ha minden jármű ugyanabban a kategóriában van, felosztjuk őket
|
||||||
|
if private_count == 0 or corporate_count == 0:
|
||||||
|
print(" ℹ️ Járművek újraelosztása 50-50% arányban...")
|
||||||
|
|
||||||
|
# Felosztás fele-fele arányban
|
||||||
|
half_index = len(user_assets) // 2
|
||||||
|
|
||||||
|
for i, asset in enumerate(user_assets):
|
||||||
|
if i < half_index:
|
||||||
|
# Első fele: maradjon privát (owner_org_id = None)
|
||||||
|
if asset.owner_org_id is not None:
|
||||||
|
asset.owner_org_id = None
|
||||||
|
print(f" 🚗 {asset.id}: Privát módra állítva")
|
||||||
|
else:
|
||||||
|
# Második fele: legyen céges (owner_org_id = target_org.id)
|
||||||
|
if asset.owner_org_id != target_org.id:
|
||||||
|
asset.owner_org_id = target_org.id
|
||||||
|
print(f" 🏢 {asset.id}: Céges flottához rendelve (Szervezet: {target_org.name})")
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
print(f" ✅ {half_index} jármű privát, {len(user_assets) - half_index} jármű céges módra állítva.")
|
||||||
|
else:
|
||||||
|
print(" ✅ A járművek már megfelelően fel vannak osztva. Nincs szükség módosításra.")
|
||||||
|
|
||||||
|
# 5. Végeredmény összefoglaló
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("VÉGEREDMÉNY ÖSSZEFOGLALÓ")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Új lekérdezés a frissített adatokhoz
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
user_assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
private_assets = [a for a in user_assets if a.owner_org_id is None]
|
||||||
|
corporate_assets = [a for a in user_assets if a.owner_org_id == target_org.id]
|
||||||
|
other_assets = [a for a in user_assets if a.owner_org_id not in [None, target_org.id]]
|
||||||
|
|
||||||
|
print(f"\n📊 TESZT FELHASZNÁLÓ ÁLLAPOTA:")
|
||||||
|
print(f" • Email: {test_user.email}")
|
||||||
|
print(f" • User ID: {test_user.id}")
|
||||||
|
print(f" • Aktív szervezet: {target_org.name} (ID: {target_org.id})")
|
||||||
|
print(f" • Szerepkör a szervezetben: ADMIN")
|
||||||
|
|
||||||
|
print(f"\n🚗 JÁRMŰVEGYÜTT ÁLLAPOTA:")
|
||||||
|
print(f" • Összes jármű: {len(user_assets)} db")
|
||||||
|
print(f" • Privát garázs (owner_org_id = NULL): {len(private_assets)} db")
|
||||||
|
print(f" • Céges flotta ({target_org.name}): {len(corporate_assets)} db")
|
||||||
|
|
||||||
|
if other_assets:
|
||||||
|
print(f" • Egyéb szervezetekhez rendelve: {len(other_assets)} db")
|
||||||
|
|
||||||
|
print(f"\n📋 PRIVÁT JÁRMŰVEK:")
|
||||||
|
for asset in private_assets[:5]: # Csak az első 5-öt mutatjuk
|
||||||
|
catalog_name = asset.catalog.make + " " + asset.catalog.model if asset.catalog else "Ismeretlen"
|
||||||
|
print(f" • {catalog_name} (Asset ID: {asset.id})")
|
||||||
|
|
||||||
|
if len(private_assets) > 5:
|
||||||
|
print(f" • ... és még {len(private_assets) - 5} további")
|
||||||
|
|
||||||
|
print(f"\n🏢 CÉGES JÁRMŰVEK:")
|
||||||
|
for asset in corporate_assets[:5]:
|
||||||
|
catalog_name = asset.catalog.make + " " + asset.catalog.model if asset.catalog else "Ismeretlen"
|
||||||
|
print(f" • {catalog_name} (Asset ID: {asset.id})")
|
||||||
|
|
||||||
|
if len(corporate_assets) > 5:
|
||||||
|
print(f" • ... és még {len(corporate_assets) - 5} további")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("A tesztadatok sikeresen előkészítve!")
|
||||||
|
print("Most tesztelhető a Garage UI switcher funkció.")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
150
backend/app/scripts/check_and_fix_garage_data_simple.py
Normal file
150
backend/app/scripts/check_and_fix_garage_data_simple.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Egyszerűbb változat: Garázs adatok ellenőrzése és javítása.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("GARÁZS ADATOK ELLENŐRZÉSE (Egyszerű változat)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# 1. Keressük meg a teszt felhasználót
|
||||||
|
print("\n1. TESZT FELHASZNÁLÓ KERESÉSE...")
|
||||||
|
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
test_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ HIBA: A teszt felhasználó nem található!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Teszt felhasználó: ID={test_user.id}, Email={test_user.email}")
|
||||||
|
|
||||||
|
# 2. Ellenőrizzük a szervezeti tagságot
|
||||||
|
print("\n2. SZERVEZETI TAGSÁG ELLENŐRZÉSE...")
|
||||||
|
org_stmt = (
|
||||||
|
select(Organization)
|
||||||
|
.join(OrganizationMember)
|
||||||
|
.where(OrganizationMember.user_id == test_user.id)
|
||||||
|
.where(Organization.is_deleted == False)
|
||||||
|
)
|
||||||
|
org_result = await db.execute(org_stmt)
|
||||||
|
user_orgs = org_result.scalars().all()
|
||||||
|
|
||||||
|
if user_orgs:
|
||||||
|
print(f" ✅ A felhasználó tagja {len(user_orgs)} szervezetnek:")
|
||||||
|
for org in user_orgs:
|
||||||
|
print(f" - {org.name} (ID: {org.id})")
|
||||||
|
target_org = user_orgs[0]
|
||||||
|
else:
|
||||||
|
print(" ℹ️ Nincs szervezet. Új létrehozása...")
|
||||||
|
# Egyszerű szervezet létrehozás
|
||||||
|
new_org = Organization(
|
||||||
|
full_name="Teszt Flotta Kft.",
|
||||||
|
name="Teszt Flotta",
|
||||||
|
display_name="Teszt Flotta Kft.",
|
||||||
|
tax_number="12345678-2-42",
|
||||||
|
country_code="HU",
|
||||||
|
folder_slug="teszt-flotta-123",
|
||||||
|
org_type="business",
|
||||||
|
status="active",
|
||||||
|
is_active=True,
|
||||||
|
owner_id=test_user.id
|
||||||
|
)
|
||||||
|
db.add(new_org)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
org_member = OrganizationMember(
|
||||||
|
organization_id=new_org.id,
|
||||||
|
user_id=test_user.id,
|
||||||
|
role="ADMIN"
|
||||||
|
)
|
||||||
|
db.add(org_member)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(new_org)
|
||||||
|
target_org = new_org
|
||||||
|
print(f" ✅ Új szervezet: {target_org.name} (ID: {target_org.id})")
|
||||||
|
|
||||||
|
# 3. Járművek lekérdezése
|
||||||
|
print("\n3. JÁRMŰVEK LEKÉRDEZÉSE...")
|
||||||
|
asset_stmt = (
|
||||||
|
select(Asset)
|
||||||
|
.where(Asset.owner_person_id == test_user.id)
|
||||||
|
)
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f" ✅ Összesen {len(assets)} jármű található.")
|
||||||
|
|
||||||
|
if not assets:
|
||||||
|
print(" ℹ️ Nincsenek járművek. Nincs mit felosztani.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Jelenlegi felosztás
|
||||||
|
private = [a for a in assets if a.owner_org_id is None]
|
||||||
|
corporate = [a for a in assets if a.owner_org_id == target_org.id]
|
||||||
|
other = [a for a in assets if a.owner_org_id not in [None, target_org.id]]
|
||||||
|
|
||||||
|
print(f"\n JELENLEGI ÁLLAPOT:")
|
||||||
|
print(f" • Privát: {len(private)} db")
|
||||||
|
print(f" • Céges ({target_org.name}): {len(corporate)} db")
|
||||||
|
if other:
|
||||||
|
print(f" • Egyéb: {len(other)} db")
|
||||||
|
|
||||||
|
# 5. Ha nincs elég adat, felosztjuk
|
||||||
|
if len(private) == 0 or len(corporate) == 0:
|
||||||
|
print("\n ℹ️ Járművek felosztása...")
|
||||||
|
half = len(assets) // 2
|
||||||
|
|
||||||
|
for i, asset in enumerate(assets):
|
||||||
|
if i < half:
|
||||||
|
asset.owner_org_id = None
|
||||||
|
else:
|
||||||
|
asset.owner_org_id = target_org.id
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
print(f" ✅ Felosztva: {half} privát, {len(assets)-half} céges")
|
||||||
|
else:
|
||||||
|
print("\n ✅ A járművek már megfelelően fel vannak osztva.")
|
||||||
|
|
||||||
|
# 6. Végeredmény
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("VÉGEREDMÉNY:")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"\n📋 TESZT FELHASZNÁLÓ:")
|
||||||
|
print(f" • Email: {test_user.email}")
|
||||||
|
print(f" • User ID: {test_user.id}")
|
||||||
|
print(f" • Szervezet: {target_org.name} (ID: {target_org.id})")
|
||||||
|
|
||||||
|
# Új lekérdezés
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
assets = asset_result.scalars().all()
|
||||||
|
private = [a for a in assets if a.owner_org_id is None]
|
||||||
|
corporate = [a for a in assets if a.owner_org_id == target_org.id]
|
||||||
|
|
||||||
|
print(f"\n🚗 JÁRMŰVEGYÜTT:")
|
||||||
|
print(f" • Összesen: {len(assets)} db")
|
||||||
|
print(f" • Privát garázs: {len(private)} db")
|
||||||
|
print(f" • Céges flotta: {len(corporate)} db")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("KÉSZ! A tesztadatok előkészítve.")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
71
backend/app/scripts/check_person_data.py
Normal file
71
backend/app/scripts/check_person_data.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Check Person data for User 28.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity.identity import User, Person
|
||||||
|
|
||||||
|
async def check_person():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# Get User 28
|
||||||
|
user_stmt = select(User).where(User.id == 28)
|
||||||
|
user_result = await db.execute(user_stmt)
|
||||||
|
test_user = user_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ User 28 not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"✅ User 28:")
|
||||||
|
print(f" - User ID: {test_user.id}")
|
||||||
|
print(f" - Person ID: {test_user.person_id}")
|
||||||
|
print(f" - Email: {test_user.email}")
|
||||||
|
|
||||||
|
# Get the Person record
|
||||||
|
if test_user.person_id:
|
||||||
|
person_stmt = select(Person).where(Person.id == test_user.person_id)
|
||||||
|
person_result = await db.execute(person_stmt)
|
||||||
|
person = person_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if person:
|
||||||
|
print(f"\n✅ Person {person.id}:")
|
||||||
|
print(f" - First name: {person.first_name}")
|
||||||
|
print(f" - Last name: {person.last_name}")
|
||||||
|
print(f" - Date of birth: {person.date_of_birth}")
|
||||||
|
print(f" - Gender: {person.gender}")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ Person with ID {test_user.person_id} not found")
|
||||||
|
else:
|
||||||
|
print("\n❌ User has no person_id")
|
||||||
|
|
||||||
|
# Check if there's a Person with ID 28
|
||||||
|
person28_stmt = select(Person).where(Person.id == 28)
|
||||||
|
person28_result = await db.execute(person28_stmt)
|
||||||
|
person28 = person28_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if person28:
|
||||||
|
print(f"\n⚠️ Person with ID 28 exists:")
|
||||||
|
print(f" - First name: {person28.first_name}")
|
||||||
|
print(f" - Last name: {person28.last_name}")
|
||||||
|
print(f" - Date of birth: {person28.date_of_birth}")
|
||||||
|
print(f" - Gender: {person28.gender}")
|
||||||
|
|
||||||
|
# Check which user is linked to this person
|
||||||
|
user_for_person28_stmt = select(User).where(User.person_id == 28)
|
||||||
|
user_for_person28_result = await db.execute(user_for_person28_stmt)
|
||||||
|
user_for_person28 = user_for_person28_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user_for_person28:
|
||||||
|
print(f" - Linked to User: {user_for_person28.email} (ID: {user_for_person28.id})")
|
||||||
|
else:
|
||||||
|
print(f" - Not linked to any user")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(check_person())
|
||||||
52
backend/app/scripts/check_vehicle_details.py
Normal file
52
backend/app/scripts/check_vehicle_details.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Check detailed vehicle data for MNO-345 and PQR-678.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
|
||||||
|
async def check_details():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# Get specific vehicles
|
||||||
|
asset_stmt = select(Asset).where(
|
||||||
|
Asset.license_plate.in_(["MNO-345", "PQR-678"])
|
||||||
|
)
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f"✅ Found {len(assets)} vehicles:")
|
||||||
|
for asset in assets:
|
||||||
|
print(f"\n Vehicle: {asset.license_plate}")
|
||||||
|
print(f" - Asset ID: {asset.id}")
|
||||||
|
print(f" - Owner Person ID: {asset.owner_person_id}")
|
||||||
|
print(f" - Operator Person ID: {asset.operator_person_id}")
|
||||||
|
print(f" - Owner Org ID: {asset.owner_org_id}")
|
||||||
|
print(f" - Operator Org ID: {asset.operator_org_id}")
|
||||||
|
print(f" - Branch ID: {asset.branch_id}")
|
||||||
|
print(f" - Current Org ID: {asset.current_organization_id}")
|
||||||
|
print(f" - Status: {asset.status}")
|
||||||
|
print(f" - Data Status: {asset.data_status}")
|
||||||
|
|
||||||
|
# Check personal mode conditions
|
||||||
|
owner_org_none = asset.owner_org_id is None
|
||||||
|
operator_org_none = asset.operator_org_id is None
|
||||||
|
print(f" - Owner Org is None: {owner_org_none}")
|
||||||
|
print(f" - Operator Org is None: {operator_org_none}")
|
||||||
|
|
||||||
|
# Check if it would appear in personal mode
|
||||||
|
would_appear_personal = (owner_org_none or operator_org_none)
|
||||||
|
print(f" - Would appear in personal mode: {would_appear_personal}")
|
||||||
|
|
||||||
|
# Check corporate mode conditions
|
||||||
|
print(f" - Would appear in corporate mode (org 15): {asset.current_organization_id == 15}")
|
||||||
|
print(f" - Would appear in corporate mode (org 21): {asset.current_organization_id == 21}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(check_details())
|
||||||
146
backend/app/scripts/create_test_vehicles.py
Normal file
146
backend/app/scripts/create_test_vehicles.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Teszt járművek létrehozása a teszt felhasználóhoz.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import uuid
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
from app.models.vehicle.vehicle_definitions import VehicleModelDefinition
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("TESZT JÁRMŰVEK LÉTREHOZÁSA")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# 1. Keressük meg a teszt felhasználót
|
||||||
|
print("\n1. TESZT FELHASZNÁLÓ KERESÉSE...")
|
||||||
|
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
test_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ HIBA: A teszt felhasználó nem található!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Teszt felhasználó: ID={test_user.id}")
|
||||||
|
|
||||||
|
# 2. Keressük meg a szervezetet
|
||||||
|
print("\n2. SZERVEZET KERESÉSE...")
|
||||||
|
org_stmt = (
|
||||||
|
select(Organization)
|
||||||
|
.join(OrganizationMember)
|
||||||
|
.where(OrganizationMember.user_id == test_user.id)
|
||||||
|
.where(Organization.is_deleted == False)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
org_result = await db.execute(org_stmt)
|
||||||
|
organization = org_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not organization:
|
||||||
|
print("❌ HIBA: Nincs szervezet a felhasználóhoz!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Szervezet: {organization.name} (ID: {organization.id})")
|
||||||
|
|
||||||
|
# 3. Keressünk néhány járműmodellt a katalógusból
|
||||||
|
print("\n3. JÁRMŰMODELLEK KERESÉSE A KATALÓGUSBÓL...")
|
||||||
|
catalog_stmt = select(VehicleModelDefinition).limit(10)
|
||||||
|
catalog_result = await db.execute(catalog_stmt)
|
||||||
|
catalog_models = catalog_result.scalars().all()
|
||||||
|
|
||||||
|
if not catalog_models:
|
||||||
|
print("❌ HIBA: Nincsenek járműmodellek a katalógusban!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ {len(catalog_models)} járműmodell található a katalógusban.")
|
||||||
|
|
||||||
|
# 4. Hozzunk létre teszt járműveket
|
||||||
|
print("\n4. TESZT JÁRMŰVEK LÉTREHOZÁSA...")
|
||||||
|
|
||||||
|
test_vehicles = [
|
||||||
|
# (név, rendszám, catalog_id, privát vagy céges)
|
||||||
|
("Privát Audi", "ABC-123", catalog_models[0].id, True),
|
||||||
|
("Privát BMW", "DEF-456", catalog_models[1].id, True),
|
||||||
|
("Privát Mercedes", "GHI-789", catalog_models[2].id, True),
|
||||||
|
("Céges Ford", "JKL-012", catalog_models[3].id, False),
|
||||||
|
("Céges Toyota", "MNO-345", catalog_models[4].id, False),
|
||||||
|
("Céges Volkswagen", "PQR-678", catalog_models[5].id, False),
|
||||||
|
]
|
||||||
|
|
||||||
|
created_count = 0
|
||||||
|
for name, license_plate, catalog_id, is_private in test_vehicles:
|
||||||
|
# Ellenőrizzük, hogy már létezik-e ilyen rendszámú jármű
|
||||||
|
existing_stmt = select(Asset).where(Asset.license_plate == license_plate)
|
||||||
|
existing_result = await db.execute(existing_stmt)
|
||||||
|
if existing_result.scalar_one_or_none():
|
||||||
|
print(f" ⚠️ '{license_plate}' rendszámú jármű már létezik, kihagyva.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Új Asset létrehozása
|
||||||
|
new_asset = Asset(
|
||||||
|
catalog_id=catalog_id,
|
||||||
|
license_plate=license_plate,
|
||||||
|
name=name,
|
||||||
|
owner_person_id=test_user.id,
|
||||||
|
owner_org_id=None if is_private else organization.id,
|
||||||
|
status="active",
|
||||||
|
price=15000000 if is_private else 20000000, # 15-20 millió HUF
|
||||||
|
currency="HUF",
|
||||||
|
individual_equipment={},
|
||||||
|
created_at=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_asset)
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
mode_text = "Privát" if is_private else "Céges"
|
||||||
|
print(f" ✅ {mode_text} jármű létrehozva: {name} ({license_plate})")
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# 5. Végeredmény
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("VÉGEREDMÉNY:")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Járművek számolása
|
||||||
|
private_stmt = select(Asset).where(
|
||||||
|
Asset.owner_person_id == test_user.id,
|
||||||
|
Asset.owner_org_id.is_(None)
|
||||||
|
)
|
||||||
|
private_result = await db.execute(private_stmt)
|
||||||
|
private_count = len(private_result.scalars().all())
|
||||||
|
|
||||||
|
corporate_stmt = select(Asset).where(
|
||||||
|
Asset.owner_person_id == test_user.id,
|
||||||
|
Asset.owner_org_id == organization.id
|
||||||
|
)
|
||||||
|
corporate_result = await db.execute(corporate_stmt)
|
||||||
|
corporate_count = len(corporate_result.scalars().all())
|
||||||
|
|
||||||
|
print(f"\n📊 ÖSSZEFOGLALÓ:")
|
||||||
|
print(f" • Felhasználó: {test_user.email}")
|
||||||
|
print(f" • Szervezet: {organization.name}")
|
||||||
|
print(f" • Új járművek létrehozva: {created_count}")
|
||||||
|
print(f" • Összes privát jármű: {private_count} db")
|
||||||
|
print(f" • Összes céges jármű: {corporate_count} db")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("KÉSZ! A teszt járművek létrehozva.")
|
||||||
|
print("Most már tesztelhető a Garage UI switcher.")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
61
backend/app/scripts/debug_scope_issue.py
Normal file
61
backend/app/scripts/debug_scope_issue.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Debug the scope_id issue.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity.identity import User
|
||||||
|
from app.core.security import decode_token
|
||||||
|
|
||||||
|
# Token from verification test (hardcoded for now, we'll get it dynamically)
|
||||||
|
TEST_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOCIsInJvbGUiOiJhZG1pbiIsInJhbmsiOjEsInNjb3BlX2xldmVsIjoib3JnYW5pemF0aW9uIiwic2NvcGVfaWQiOiIyOCIsImV4cCI6MTc3NDg2MjQwMCwiaWF0IjoxNzQzMzI2NDAwLCJ0eXBlIjoiYWNjZXNzIn0.4Q9n2vQ8q3V7X6Y5Z8A9B0C1D2E3F4G5H6I7J8K9L0"
|
||||||
|
|
||||||
|
async def debug():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# Decode token
|
||||||
|
payload = decode_token(TEST_TOKEN)
|
||||||
|
print(f"✅ Token payload:")
|
||||||
|
print(f" - sub: {payload.get('sub')}")
|
||||||
|
print(f" - scope_id: {payload.get('scope_id')}")
|
||||||
|
print(f" - scope_level: {payload.get('scope_level')}")
|
||||||
|
print(f" - role: {payload.get('role')}")
|
||||||
|
|
||||||
|
# Get user from database
|
||||||
|
user_id = payload.get('sub')
|
||||||
|
if user_id:
|
||||||
|
user_stmt = select(User).where(User.id == int(user_id))
|
||||||
|
user_result = await db.execute(user_stmt)
|
||||||
|
user = user_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
print(f"\n✅ User from database (ID: {user.id}):")
|
||||||
|
print(f" - scope_id: {user.scope_id}")
|
||||||
|
print(f" - scope_level: {user.scope_level}")
|
||||||
|
print(f" - person_id: {user.person_id}")
|
||||||
|
|
||||||
|
# Check what the assets endpoint would see
|
||||||
|
print(f"\n🔍 Assets endpoint logic:")
|
||||||
|
print(f" - current_user.scope_id: {user.scope_id}")
|
||||||
|
print(f" - Type: {type(user.scope_id)}")
|
||||||
|
print(f" - Is None? {user.scope_id is None}")
|
||||||
|
print(f" - == 'None'? {user.scope_id == 'None'}")
|
||||||
|
print(f" - == ''? {user.scope_id == ''}")
|
||||||
|
|
||||||
|
if user.scope_id is None:
|
||||||
|
print(" → Would go to PERSONAL mode")
|
||||||
|
else:
|
||||||
|
print(" → Would go to CORPORATE mode")
|
||||||
|
try:
|
||||||
|
scope_org_id = int(user.scope_id)
|
||||||
|
print(f" → scope_org_id: {scope_org_id}")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
print(f" → scope_org_id: None (invalid)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(debug())
|
||||||
104
backend/app/scripts/diagnose_vehicle_filtering.py
Normal file
104
backend/app/scripts/diagnose_vehicle_filtering.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Diagnostic script to understand why vehicle filtering isn't working correctly.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
from sqlalchemy import select, or_
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
from app.models.identity.identity import User
|
||||||
|
|
||||||
|
async def diagnose():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# Get User 28
|
||||||
|
user_stmt = select(User).where(User.id == 28)
|
||||||
|
user_result = await db.execute(user_stmt)
|
||||||
|
test_user = user_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ User 28 not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"✅ Found User 28:")
|
||||||
|
print(f" - User ID: {test_user.id}")
|
||||||
|
print(f" - Person ID: {test_user.person_id}")
|
||||||
|
print(f" - Email: {test_user.email}")
|
||||||
|
|
||||||
|
# Get all vehicles
|
||||||
|
asset_stmt = select(Asset).where(
|
||||||
|
or_(
|
||||||
|
Asset.license_plate == "MNO-345",
|
||||||
|
Asset.license_plate == "PQR-678"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f"\n✅ Found {len(assets)} vehicles:")
|
||||||
|
for asset in assets:
|
||||||
|
print(f"\n Vehicle: {asset.license_plate}")
|
||||||
|
print(f" - Asset ID: {asset.id}")
|
||||||
|
print(f" - Owner Person ID: {asset.owner_person_id}")
|
||||||
|
print(f" - Operator Person ID: {asset.operator_person_id}")
|
||||||
|
print(f" - Owner Org ID: {asset.owner_org_id}")
|
||||||
|
print(f" - Operator Org ID: {asset.operator_org_id}")
|
||||||
|
print(f" - Branch ID: {asset.branch_id}")
|
||||||
|
print(f" - Current Org ID: {asset.current_organization_id}")
|
||||||
|
|
||||||
|
# Check if it matches user's person_id
|
||||||
|
matches_owner = asset.owner_person_id == test_user.person_id
|
||||||
|
matches_operator = asset.operator_person_id == test_user.person_id
|
||||||
|
print(f" - Matches owner_person_id ({test_user.person_id}): {matches_owner}")
|
||||||
|
print(f" - Matches operator_person_id ({test_user.person_id}): {matches_operator}")
|
||||||
|
|
||||||
|
# Now test the actual query logic from assets.py
|
||||||
|
print(f"\n🔍 Testing the actual query logic:")
|
||||||
|
|
||||||
|
# Personal mode query (from assets.py lines 41-57)
|
||||||
|
stmt = (
|
||||||
|
select(Asset)
|
||||||
|
.where(
|
||||||
|
or_(
|
||||||
|
Asset.owner_org_id.is_(None),
|
||||||
|
Asset.operator_org_id.is_(None)
|
||||||
|
),
|
||||||
|
or_(
|
||||||
|
Asset.owner_person_id == test_user.person_id,
|
||||||
|
Asset.operator_person_id == test_user.person_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(Asset.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
personal_assets = result.scalars().all()
|
||||||
|
|
||||||
|
print(f" Personal mode query returns {len(personal_assets)} vehicles:")
|
||||||
|
for asset in personal_assets:
|
||||||
|
print(f" - {asset.license_plate}")
|
||||||
|
|
||||||
|
# Corporate mode query (for organization 15)
|
||||||
|
print(f"\n🔍 Testing corporate mode query (org_id = 15):")
|
||||||
|
stmt = (
|
||||||
|
select(Asset)
|
||||||
|
.where(
|
||||||
|
Asset.current_organization_id == 15,
|
||||||
|
Asset.branch_id.is_not(None)
|
||||||
|
)
|
||||||
|
.order_by(Asset.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
corporate_assets = result.scalars().all()
|
||||||
|
|
||||||
|
print(f" Corporate mode query returns {len(corporate_assets)} vehicles:")
|
||||||
|
for asset in corporate_assets:
|
||||||
|
print(f" - {asset.license_plate}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(diagnose())
|
||||||
100
backend/app/scripts/fix_asset_person_ids.py
Normal file
100
backend/app/scripts/fix_asset_person_ids.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Fix asset person IDs - update owner_person_id from User ID to Person ID.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
from app.models.identity.identity import User
|
||||||
|
|
||||||
|
async def fix_asset_person_ids():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# Get User 28
|
||||||
|
user_stmt = select(User).where(User.id == 28)
|
||||||
|
user_result = await db.execute(user_stmt)
|
||||||
|
test_user = user_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ User 28 not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"✅ Found User 28:")
|
||||||
|
print(f" - User ID: {test_user.id}")
|
||||||
|
print(f" - Person ID: {test_user.person_id}")
|
||||||
|
print(f" - Email: {test_user.email}")
|
||||||
|
|
||||||
|
if not test_user.person_id:
|
||||||
|
print("❌ User has no person_id")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find assets with owner_person_id = 28 (User ID)
|
||||||
|
asset_stmt = select(Asset).where(
|
||||||
|
Asset.owner_person_id == 28
|
||||||
|
)
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f"\n✅ Found {len(assets)} assets with owner_person_id = 28:")
|
||||||
|
for asset in assets:
|
||||||
|
print(f" - {asset.license_plate} (Asset ID: {asset.id})")
|
||||||
|
|
||||||
|
if assets:
|
||||||
|
# Update them to have person_id = 29
|
||||||
|
update_stmt = (
|
||||||
|
update(Asset)
|
||||||
|
.where(Asset.owner_person_id == 28)
|
||||||
|
.values(owner_person_id=test_user.person_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await db.execute(update_stmt)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
print(f"\n✅ Updated {result.rowcount} assets:")
|
||||||
|
print(f" - Changed owner_person_id from 28 to {test_user.person_id}")
|
||||||
|
|
||||||
|
# Verify the update
|
||||||
|
asset_stmt = select(Asset).where(
|
||||||
|
Asset.owner_person_id == test_user.person_id
|
||||||
|
)
|
||||||
|
asset_result = await db.execute(asset_stmt)
|
||||||
|
updated_assets = asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f"\n✅ Verification - Found {len(updated_assets)} assets with owner_person_id = {test_user.person_id}:")
|
||||||
|
for asset in updated_assets:
|
||||||
|
print(f" - {asset.license_plate}")
|
||||||
|
else:
|
||||||
|
print("\nℹ️ No assets found with owner_person_id = 28")
|
||||||
|
|
||||||
|
# Also check for operator_person_id = 28
|
||||||
|
operator_asset_stmt = select(Asset).where(
|
||||||
|
Asset.operator_person_id == 28
|
||||||
|
)
|
||||||
|
operator_asset_result = await db.execute(operator_asset_stmt)
|
||||||
|
operator_assets = operator_asset_result.scalars().all()
|
||||||
|
|
||||||
|
print(f"\n✅ Found {len(operator_assets)} assets with operator_person_id = 28:")
|
||||||
|
for asset in operator_assets:
|
||||||
|
print(f" - {asset.license_plate} (Asset ID: {asset.id})")
|
||||||
|
|
||||||
|
if operator_assets:
|
||||||
|
# Update them to have person_id = 29
|
||||||
|
update_stmt = (
|
||||||
|
update(Asset)
|
||||||
|
.where(Asset.operator_person_id == 28)
|
||||||
|
.values(operator_person_id=test_user.person_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await db.execute(update_stmt)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
print(f"\n✅ Updated {result.rowcount} operator assets:")
|
||||||
|
print(f" - Changed operator_person_id from 28 to {test_user.person_id}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(fix_asset_person_ids())
|
||||||
227
backend/app/scripts/fix_test_user_garage.py
Normal file
227
backend/app/scripts/fix_test_user_garage.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Fix test user garage data and split vehicles between private and org branches.
|
||||||
|
|
||||||
|
This script:
|
||||||
|
1. Fetches User 28 (tester_pro@profibot.hu)
|
||||||
|
2. Creates a Private Organization for User 28 (if missing)
|
||||||
|
3. Creates a Private Branch (Garage) named "Teszt Pro - Saját Garázs"
|
||||||
|
4. Fetches the 2 vehicles owned by User 28 (MNO-345 and PQR-678)
|
||||||
|
5. Splits them: Assigns MNO-345 to the newly created Private Branch
|
||||||
|
6. Keeps PQR-678 in the Org 15 Branch (b2060e1d...)
|
||||||
|
7. Commits changes
|
||||||
|
|
||||||
|
Run inside sf_api container:
|
||||||
|
docker compose exec sf_api python -m app.scripts.fix_test_user_garage
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
# Add the backend directory to the path
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.models.marketplace.organization import Organization, OrganizationMember, Branch, OrgType, OrgUserRole
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main execution logic."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("FIX TEST USER GARAGE DATA SCRIPT")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# 1. Fetch User 28 (tester_pro@profibot.hu)
|
||||||
|
print("\n1. FETCHING TEST USER...")
|
||||||
|
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
test_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not test_user:
|
||||||
|
print("❌ ERROR: Test user (tester_pro@profibot.hu) not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Test user found: ID={test_user.id}, Email={test_user.email}")
|
||||||
|
|
||||||
|
# 2. Check if user already has a private organization
|
||||||
|
print("\n2. CHECKING FOR PRIVATE ORGANIZATION...")
|
||||||
|
org_stmt = (
|
||||||
|
select(Organization)
|
||||||
|
.join(OrganizationMember)
|
||||||
|
.where(OrganizationMember.user_id == test_user.id)
|
||||||
|
.where(Organization.org_type == OrgType.individual)
|
||||||
|
.where(Organization.is_deleted == False)
|
||||||
|
)
|
||||||
|
result = await db.execute(org_stmt)
|
||||||
|
private_org = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not private_org:
|
||||||
|
print(" ⚠️ No private organization found, creating one...")
|
||||||
|
|
||||||
|
# Create private organization
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
private_org = Organization(
|
||||||
|
full_name=f"Private Organization for {test_user.email}",
|
||||||
|
name=f"Private_{test_user.id}",
|
||||||
|
display_name=f"Teszt Pro - Privát",
|
||||||
|
folder_slug=f"priv_{test_user.id}",
|
||||||
|
org_type=OrgType.individual,
|
||||||
|
status="active",
|
||||||
|
is_active=True,
|
||||||
|
is_deleted=False,
|
||||||
|
country_code="HU",
|
||||||
|
language="hu",
|
||||||
|
default_currency="HUF",
|
||||||
|
first_registered_at=now,
|
||||||
|
current_lifecycle_started_at=now,
|
||||||
|
subscription_plan="FREE",
|
||||||
|
base_asset_limit=1,
|
||||||
|
purchased_extra_slots=0,
|
||||||
|
notification_settings={"notify_owner": True, "alert_days_before": [30, 15, 7, 1]},
|
||||||
|
external_integration_config={},
|
||||||
|
created_at=now,
|
||||||
|
is_ownership_transferable=True
|
||||||
|
)
|
||||||
|
db.add(private_org)
|
||||||
|
await db.flush() # Get the ID
|
||||||
|
|
||||||
|
# Add user as owner of the organization
|
||||||
|
org_member = OrganizationMember(
|
||||||
|
organization_id=private_org.id,
|
||||||
|
user_id=test_user.id,
|
||||||
|
person_id=test_user.person_id,
|
||||||
|
role=OrgUserRole.OWNER,
|
||||||
|
is_verified=True
|
||||||
|
)
|
||||||
|
db.add(org_member)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
print(f" ✅ Created private organization: ID={private_org.id}, Name={private_org.name}")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Private organization already exists: ID={private_org.id}, Name={private_org.name}")
|
||||||
|
|
||||||
|
# 3. Check/create private branch (garage)
|
||||||
|
print("\n3. CHECKING/CREATING PRIVATE BRANCH (GARAGE)...")
|
||||||
|
branch_stmt = select(Branch).where(
|
||||||
|
Branch.organization_id == private_org.id,
|
||||||
|
Branch.name.ilike("%Teszt Pro - Saját Garázs%"),
|
||||||
|
Branch.is_deleted == False
|
||||||
|
)
|
||||||
|
result = await db.execute(branch_stmt)
|
||||||
|
private_branch = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not private_branch:
|
||||||
|
private_branch = Branch(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
organization_id=private_org.id,
|
||||||
|
name="Teszt Pro - Saját Garázs",
|
||||||
|
is_main=True,
|
||||||
|
status="active",
|
||||||
|
is_deleted=False,
|
||||||
|
postal_code="1234",
|
||||||
|
city="Budapest",
|
||||||
|
street_name="Teszt utca",
|
||||||
|
house_number="1"
|
||||||
|
)
|
||||||
|
db.add(private_branch)
|
||||||
|
await db.commit()
|
||||||
|
print(f" ✅ Created private branch: ID={private_branch.id}, Name={private_branch.name}")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Private branch already exists: ID={private_branch.id}, Name={private_branch.name}")
|
||||||
|
|
||||||
|
# 4. Fetch the 2 vehicles by license plate (regardless of owner)
|
||||||
|
print("\n4. FETCHING VEHICLES BY LICENSE PLATE...")
|
||||||
|
|
||||||
|
# Find vehicles by license plate
|
||||||
|
vehicles_stmt = select(Asset).where(
|
||||||
|
Asset.license_plate.in_(["MNO-345", "PQR-678"])
|
||||||
|
)
|
||||||
|
result = await db.execute(vehicles_stmt)
|
||||||
|
vehicles = result.scalars().all()
|
||||||
|
|
||||||
|
print(f" Found {len(vehicles)} vehicles with plates MNO-345 or PQR-678")
|
||||||
|
|
||||||
|
# 5. Find Org 15 branch (b2060e1d...)
|
||||||
|
print("\n5. FINDING ORG 15 BRANCH...")
|
||||||
|
# First find Org 15
|
||||||
|
org15_stmt = select(Organization).where(
|
||||||
|
Organization.id == 15,
|
||||||
|
Organization.is_deleted == False
|
||||||
|
)
|
||||||
|
result = await db.execute(org15_stmt)
|
||||||
|
org15 = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not org15:
|
||||||
|
print("❌ ERROR: Organization 15 not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Organization 15 found: ID={org15.id}, Name={org15.name}")
|
||||||
|
|
||||||
|
# Find a branch in Org 15
|
||||||
|
org15_branch_stmt = select(Branch).where(
|
||||||
|
Branch.organization_id == 15,
|
||||||
|
Branch.is_deleted == False
|
||||||
|
).limit(1)
|
||||||
|
result = await db.execute(org15_branch_stmt)
|
||||||
|
org15_branch = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not org15_branch:
|
||||||
|
print("❌ ERROR: No branch found in Organization 15!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" ✅ Org 15 branch found: ID={org15_branch.id}, Name={org15_branch.name}")
|
||||||
|
|
||||||
|
# 6. Split vehicles
|
||||||
|
print("\n6. SPLITTING VEHICLES BETWEEN BRANCHES...")
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
for vehicle in vehicles:
|
||||||
|
if vehicle.license_plate == "MNO-345":
|
||||||
|
# Assign to private branch
|
||||||
|
vehicle.branch_id = private_branch.id
|
||||||
|
vehicle.current_organization_id = private_org.id
|
||||||
|
print(f" ✅ Assigned MNO-345 to private branch: {private_branch.name}")
|
||||||
|
updated_count += 1
|
||||||
|
elif vehicle.license_plate == "PQR-678":
|
||||||
|
# Keep in Org 15 branch
|
||||||
|
vehicle.branch_id = org15_branch.id
|
||||||
|
vehicle.current_organization_id = 15
|
||||||
|
print(f" ✅ Kept PQR-678 in Org 15 branch: {org15_branch.name}")
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
if updated_count > 0:
|
||||||
|
await db.commit()
|
||||||
|
print(f"\n✅ Successfully updated {updated_count} vehicles")
|
||||||
|
else:
|
||||||
|
print("\n⚠️ No vehicles needed updating")
|
||||||
|
|
||||||
|
# 7. Verify the split
|
||||||
|
print("\n7. VERIFICATION...")
|
||||||
|
for plate in ["MNO-345", "PQR-678"]:
|
||||||
|
verify_stmt = select(Asset).where(Asset.license_plate == plate).options(selectinload(Asset.catalog))
|
||||||
|
result = await db.execute(verify_stmt)
|
||||||
|
vehicle = result.scalar_one_or_none()
|
||||||
|
if vehicle:
|
||||||
|
branch_name = "Unknown"
|
||||||
|
if vehicle.branch_id == private_branch.id:
|
||||||
|
branch_name = "Private Branch"
|
||||||
|
elif vehicle.branch_id == org15_branch.id:
|
||||||
|
branch_name = "Org 15 Branch"
|
||||||
|
|
||||||
|
print(f" {plate}: Branch ID={vehicle.branch_id} ({branch_name}), Org ID={vehicle.current_organization_id}")
|
||||||
|
else:
|
||||||
|
print(f" {plate}: Not found")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("SCRIPT COMPLETED SUCCESSFULLY")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
190
backend/app/scripts/migrate_vehicles_to_garages.py
Normal file
190
backend/app/scripts/migrate_vehicles_to_garages.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migration script to assign existing vehicles to their organization's default garage (branch).
|
||||||
|
This fixes the issue where existing vehicles have branch_id = NULL after the column was added.
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
1. For each Asset with owner_org_id or operator_org_id
|
||||||
|
2. Find the default Branch (Garage) for that Organization (is_main = True)
|
||||||
|
3. Update Asset.branch_id to that Branch's UUID
|
||||||
|
4. If no default branch exists, create one
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.database import AsyncSessionLocal
|
||||||
|
from app.models.vehicle.asset import Asset
|
||||||
|
from app.models.marketplace.organization import Branch, Organization
|
||||||
|
from app.models.identity import User
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def get_or_create_default_branch(session: AsyncSession, organization_id: int) -> Branch:
|
||||||
|
"""Get the default branch (is_main = True) for an organization, create if doesn't exist."""
|
||||||
|
# Try to find existing default branch
|
||||||
|
stmt = select(Branch).where(
|
||||||
|
Branch.organization_id == organization_id,
|
||||||
|
Branch.is_main == True,
|
||||||
|
Branch.is_deleted == False
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
branch = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if branch:
|
||||||
|
logger.info(f"Found default branch {branch.id} for organization {organization_id}")
|
||||||
|
return branch
|
||||||
|
|
||||||
|
# If no default branch exists, create one
|
||||||
|
logger.warning(f"No default branch found for organization {organization_id}, creating one...")
|
||||||
|
|
||||||
|
# Get organization name for branch naming
|
||||||
|
org_stmt = select(Organization).where(Organization.id == organization_id)
|
||||||
|
org_result = await session.execute(org_stmt)
|
||||||
|
organization = org_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
org_name = organization.name if organization else f"Organization {organization_id}"
|
||||||
|
|
||||||
|
# Create default branch
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
new_branch = Branch(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
organization_id=organization_id,
|
||||||
|
name=f"{org_name} - Main Garage",
|
||||||
|
is_main=True,
|
||||||
|
status="active",
|
||||||
|
is_deleted=False,
|
||||||
|
created_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(new_branch)
|
||||||
|
await session.flush()
|
||||||
|
logger.info(f"Created default branch {new_branch.id} for organization {organization_id}")
|
||||||
|
|
||||||
|
return new_branch
|
||||||
|
|
||||||
|
async def migrate_vehicles_to_garages():
|
||||||
|
"""Main migration function."""
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
# Get all assets that have organization ownership but no branch_id
|
||||||
|
stmt = select(Asset).where(
|
||||||
|
(Asset.owner_org_id.is_not(None) | Asset.operator_org_id.is_not(None)),
|
||||||
|
Asset.branch_id.is_(None)
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
assets = result.scalars().all()
|
||||||
|
|
||||||
|
logger.info(f"Found {len(assets)} assets without branch assignment")
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
for asset in assets:
|
||||||
|
# Determine which organization to use (prefer owner, fallback to operator)
|
||||||
|
org_id = asset.owner_org_id or asset.operator_org_id
|
||||||
|
|
||||||
|
if not org_id:
|
||||||
|
logger.warning(f"Asset {asset.id} has no organization reference, skipping")
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get or create default branch for the organization
|
||||||
|
branch = await get_or_create_default_branch(session, org_id)
|
||||||
|
|
||||||
|
# Update the asset
|
||||||
|
update_stmt = (
|
||||||
|
update(Asset)
|
||||||
|
.where(Asset.id == asset.id)
|
||||||
|
.values(branch_id=branch.id, relocation_performed=True)
|
||||||
|
)
|
||||||
|
await session.execute(update_stmt)
|
||||||
|
|
||||||
|
logger.info(f"Updated asset {asset.id} with branch {branch.id} (org {org_id})")
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
# Commit all changes
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
logger.info(f"Migration completed: {updated_count} assets updated, {skipped_count} skipped")
|
||||||
|
|
||||||
|
# Also update assets that already have branch_id but need relocation_performed flag
|
||||||
|
if updated_count > 0:
|
||||||
|
stmt = select(Asset).where(
|
||||||
|
Asset.branch_id.is_not(None),
|
||||||
|
Asset.relocation_performed == False
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
assets_without_flag = result.scalars().all()
|
||||||
|
|
||||||
|
for asset in assets_without_flag:
|
||||||
|
update_stmt = (
|
||||||
|
update(Asset)
|
||||||
|
.where(Asset.id == asset.id)
|
||||||
|
.values(relocation_performed=True)
|
||||||
|
)
|
||||||
|
await session.execute(update_stmt)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info(f"Updated relocation_performed flag for {len(assets_without_flag)} assets")
|
||||||
|
|
||||||
|
return updated_count
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def verify_migration():
|
||||||
|
"""Verify the migration results."""
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
# Count assets with branch_id
|
||||||
|
stmt = select(Asset).where(Asset.branch_id.is_not(None))
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
assets_with_branch = result.scalars().all()
|
||||||
|
|
||||||
|
# Count assets without branch_id but with organizations
|
||||||
|
stmt = select(Asset).where(
|
||||||
|
(Asset.owner_org_id.is_not(None) | Asset.operator_org_id.is_not(None)),
|
||||||
|
Asset.branch_id.is_(None)
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
assets_still_missing = result.scalars().all()
|
||||||
|
|
||||||
|
logger.info(f"Verification:")
|
||||||
|
logger.info(f" - Assets with branch_id: {len(assets_with_branch)}")
|
||||||
|
logger.info(f" - Assets still missing branch_id: {len(assets_still_missing)}")
|
||||||
|
|
||||||
|
if assets_still_missing:
|
||||||
|
logger.warning("Some assets still missing branch_id:")
|
||||||
|
for asset in assets_still_missing[:5]: # Show first 5
|
||||||
|
logger.warning(f" Asset {asset.id}: owner_org={asset.owner_org_id}, operator_org={asset.operator_org_id}")
|
||||||
|
|
||||||
|
return len(assets_with_branch), len(assets_still_missing)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logger.info("Starting vehicle-to-garage migration...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run migration
|
||||||
|
updated = asyncio.run(migrate_vehicles_to_garages())
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
with_branch, missing = asyncio.run(verify_migration())
|
||||||
|
|
||||||
|
if missing == 0:
|
||||||
|
logger.info("✅ Migration successful! All organizational vehicles now have branch assignments.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ Migration incomplete: {missing} assets still lack branch_id")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Migration failed: {e}")
|
||||||
|
sys.exit(1)
|
||||||
108
backend/app/scripts/test_assets_api_directly.py
Normal file
108
backend/app/scripts/test_assets_api_directly.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test the assets API endpoint directly.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
|
||||||
|
async def test_assets_api():
|
||||||
|
base_url = "http://sf_api:8000"
|
||||||
|
|
||||||
|
# First, login to get token
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# Login
|
||||||
|
login_data = {
|
||||||
|
"username": "tester_pro@profibot.hu",
|
||||||
|
"password": "Test123!"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("1. Logging in...")
|
||||||
|
async with session.post(f"{base_url}/api/v1/auth/login", data=login_data) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f"❌ Login failed: {resp.status}")
|
||||||
|
text = await resp.text()
|
||||||
|
print(f"Response: {text}")
|
||||||
|
return
|
||||||
|
login_result = await resp.json()
|
||||||
|
token = login_result["access_token"]
|
||||||
|
print(f"✅ Login successful, token: {token[:50]}...")
|
||||||
|
|
||||||
|
# Set personal mode (scope_id = null)
|
||||||
|
print("\n2. Setting personal mode (scope_id = null)...")
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
patch_data = {"organization_id": None}
|
||||||
|
|
||||||
|
async with session.patch(
|
||||||
|
f"{base_url}/api/v1/users/me/active-organization",
|
||||||
|
json=patch_data,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f"❌ PATCH failed: {resp.status}")
|
||||||
|
text = await resp.text()
|
||||||
|
print(f"Response: {text}")
|
||||||
|
return
|
||||||
|
patch_result = await resp.json()
|
||||||
|
print(f"✅ PATCH successful")
|
||||||
|
print(f" scope_id: {patch_result.get('scope_id')}")
|
||||||
|
print(f" scope_level: {patch_result.get('scope_level')}")
|
||||||
|
|
||||||
|
# Get vehicles in personal mode
|
||||||
|
print("\n3. Getting vehicles in personal mode...")
|
||||||
|
async with session.get(
|
||||||
|
f"{base_url}/api/v1/assets/vehicles",
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f"❌ GET vehicles failed: {resp.status}")
|
||||||
|
text = await resp.text()
|
||||||
|
print(f"Response: {text}")
|
||||||
|
return
|
||||||
|
vehicles_result = await resp.json()
|
||||||
|
print(f"✅ GET vehicles successful")
|
||||||
|
print(f" Found {len(vehicles_result)} vehicles")
|
||||||
|
for i, vehicle in enumerate(vehicles_result[:5]): # Show first 5
|
||||||
|
print(f" {i+1}. {vehicle.get('license_plate')} (ID: {vehicle.get('id')})")
|
||||||
|
if len(vehicles_result) > 5:
|
||||||
|
print(f" ... and {len(vehicles_result) - 5} more")
|
||||||
|
|
||||||
|
# Now test corporate mode (org_id = 15)
|
||||||
|
print("\n4. Setting corporate mode (org_id = 15)...")
|
||||||
|
patch_data_corp = {"organization_id": "15"}
|
||||||
|
|
||||||
|
async with session.patch(
|
||||||
|
f"{base_url}/api/v1/users/me/active-organization",
|
||||||
|
json=patch_data_corp,
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f"❌ PATCH corporate failed: {resp.status}")
|
||||||
|
text = await resp.text()
|
||||||
|
print(f"Response: {text}")
|
||||||
|
return
|
||||||
|
patch_corp_result = await resp.json()
|
||||||
|
print(f"✅ PATCH corporate successful")
|
||||||
|
print(f" scope_id: {patch_corp_result.get('scope_id')}")
|
||||||
|
|
||||||
|
# Get vehicles in corporate mode
|
||||||
|
print("\n5. Getting vehicles in corporate mode...")
|
||||||
|
async with session.get(
|
||||||
|
f"{base_url}/api/v1/assets/vehicles",
|
||||||
|
headers=headers
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f"❌ GET corporate vehicles failed: {resp.status}")
|
||||||
|
text = await resp.text()
|
||||||
|
print(f"Response: {text}")
|
||||||
|
return
|
||||||
|
vehicles_corp_result = await resp.json()
|
||||||
|
print(f"✅ GET corporate vehicles successful")
|
||||||
|
print(f" Found {len(vehicles_corp_result)} vehicles")
|
||||||
|
for i, vehicle in enumerate(vehicles_corp_result[:5]):
|
||||||
|
print(f" {i+1}. {vehicle.get('license_plate')}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_assets_api())
|
||||||
175
backend/app/scripts/verify_scope_switcher.py
Normal file
175
backend/app/scripts/verify_scope_switcher.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Backend verification test for scope switcher functionality.
|
||||||
|
|
||||||
|
Simulates the frontend:
|
||||||
|
1. Login as User 28 (tester_pro@profibot.hu) - get JWT token
|
||||||
|
2. Call PATCH /active-organization with organization_id = null
|
||||||
|
3. Call GET /vehicles - assert it returns exactly 1 car (MNO-345)
|
||||||
|
4. Call PATCH /active-organization with organization_id = 15
|
||||||
|
5. Call GET /vehicles - assert it returns exactly 1 car (PQR-678)
|
||||||
|
|
||||||
|
Run inside sf_api container:
|
||||||
|
docker compose exec sf_api python -m app.scripts.verify_scope_switcher
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
# Add the backend directory to the path
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.core.security import create_tokens
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
async def get_auth_token() -> str:
|
||||||
|
"""Get JWT token for test user by simulating login."""
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# Find the test user
|
||||||
|
from sqlalchemy import select
|
||||||
|
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise Exception("Test user not found")
|
||||||
|
|
||||||
|
# Create token directly (simulating login)
|
||||||
|
from app.core.security import DEFAULT_RANK_MAP
|
||||||
|
ranks = DEFAULT_RANK_MAP # Simplified
|
||||||
|
|
||||||
|
token_data = {
|
||||||
|
"sub": str(user.id),
|
||||||
|
"role": user.role.value if hasattr(user.role, 'value') else str(user.role),
|
||||||
|
"rank": ranks.get(user.role.value.upper(), 10),
|
||||||
|
"scope_level": user.scope_level or "individual",
|
||||||
|
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
access_token, _ = create_tokens(data=token_data)
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
async def make_api_request(method: str, endpoint: str, token: str, data: Dict = None) -> Dict[str, Any]:
|
||||||
|
"""Make HTTP request to the API."""
|
||||||
|
base_url = "http://localhost:8000/api/v1"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
url = f"{base_url}{endpoint}"
|
||||||
|
|
||||||
|
if method == "GET":
|
||||||
|
response = await client.get(url, headers=headers)
|
||||||
|
elif method == "PATCH":
|
||||||
|
response = await client.patch(url, headers=headers, json=data)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported method: {method}")
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main verification test."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("SCOPE SWITCHER VERIFICATION TEST")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Get auth token
|
||||||
|
print("\n1. AUTHENTICATING AS TEST USER...")
|
||||||
|
token = await get_auth_token()
|
||||||
|
print(f" ✅ Token obtained (length: {len(token)})")
|
||||||
|
|
||||||
|
# 2. Test personal mode (organization_id = null)
|
||||||
|
print("\n2. TESTING PERSONAL MODE (organization_id = null)...")
|
||||||
|
patch_data = {"organization_id": None}
|
||||||
|
patch_response = await make_api_request("PATCH", "/users/me/active-organization", token, patch_data)
|
||||||
|
print(f" ✅ PATCH response: {json.dumps(patch_response, indent=2)}")
|
||||||
|
|
||||||
|
# Check if scope_id was updated
|
||||||
|
if patch_response.get("scope_id") is not None:
|
||||||
|
print(" ❌ ERROR: scope_id should be null in personal mode!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Get vehicles in personal mode
|
||||||
|
print("\n3. GETTING VEHICLES IN PERSONAL MODE...")
|
||||||
|
vehicles_response = await make_api_request("GET", "/assets/vehicles", token)
|
||||||
|
vehicles_count = len(vehicles_response)
|
||||||
|
print(f" ✅ Found {vehicles_count} vehicles")
|
||||||
|
|
||||||
|
# Check license plates
|
||||||
|
license_plates = [v.get("license_plate") for v in vehicles_response if v.get("license_plate")]
|
||||||
|
print(f" ✅ License plates: {license_plates}")
|
||||||
|
|
||||||
|
# Should have exactly 1 car (MNO-345)
|
||||||
|
if vehicles_count != 1:
|
||||||
|
print(f" ❌ ERROR: Expected 1 vehicle in personal mode, got {vehicles_count}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "MNO-345" not in license_plates:
|
||||||
|
print(f" ❌ ERROR: Expected MNO-345 in personal mode, got {license_plates}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(" ✅ PERSONAL MODE TEST PASSED: Exactly 1 car (MNO-345)")
|
||||||
|
|
||||||
|
# 4. Test corporate mode (organization_id = 15)
|
||||||
|
print("\n4. TESTING CORPORATE MODE (organization_id = 15)...")
|
||||||
|
patch_data = {"organization_id": 15}
|
||||||
|
patch_response = await make_api_request("PATCH", "/users/me/active-organization", token, patch_data)
|
||||||
|
print(f" ✅ PATCH response: {json.dumps(patch_response, indent=2)}")
|
||||||
|
|
||||||
|
# Check if scope_id was updated
|
||||||
|
if patch_response.get("scope_id") != "15":
|
||||||
|
print(f" ❌ ERROR: scope_id should be '15', got {patch_response.get('scope_id')}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. Get vehicles in corporate mode
|
||||||
|
print("\n5. GETTING VEHICLES IN CORPORATE MODE...")
|
||||||
|
vehicles_response = await make_api_request("GET", "/assets/vehicles", token)
|
||||||
|
vehicles_count = len(vehicles_response)
|
||||||
|
print(f" ✅ Found {vehicles_count} vehicles")
|
||||||
|
|
||||||
|
# Check license plates
|
||||||
|
license_plates = [v.get("license_plate") for v in vehicles_response if v.get("license_plate")]
|
||||||
|
print(f" ✅ License plates: {license_plates}")
|
||||||
|
|
||||||
|
# Should have exactly 1 car (PQR-678)
|
||||||
|
if vehicles_count != 1:
|
||||||
|
print(f" ❌ ERROR: Expected 1 vehicle in corporate mode, got {vehicles_count}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "PQR-678" not in license_plates:
|
||||||
|
print(f" ❌ ERROR: Expected PQR-678 in corporate mode, got {license_plates}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(" ✅ CORPORATE MODE TEST PASSED: Exactly 1 car (PQR-678)")
|
||||||
|
|
||||||
|
# 6. Final verification
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✅ ALL TESTS PASSED!")
|
||||||
|
print("=" * 60)
|
||||||
|
print("\nSUMMARY:")
|
||||||
|
print("- Personal mode (scope_id = null): Shows 1 vehicle (MNO-345)")
|
||||||
|
print("- Corporate mode (scope_id = 15): Shows 1 vehicle (PQR-678)")
|
||||||
|
print("- Scope switcher endpoint works correctly")
|
||||||
|
print("- Vehicle filtering by branch/organization works correctly")
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
print(f"\n❌ HTTP ERROR: {e}")
|
||||||
|
print(f"Response: {e.response.text}")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ ERROR: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
266
backend/app/tests/test_issues_176_177_internal.py
Normal file
266
backend/app/tests/test_issues_176_177_internal.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Internal test script for Issues #176 and #177
|
||||||
|
This script runs inside the sf_api container using only Python standard library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configuration - internal Docker network
|
||||||
|
BASE_URL = "http://sf_api:8000/api/v1"
|
||||||
|
# Using a test user that should exist in the dev database
|
||||||
|
TEST_EMAIL = "tester_pro@profibot.hu"
|
||||||
|
TEST_PASSWORD = "Password123!"
|
||||||
|
|
||||||
|
def make_request(method, url, data=None, headers=None, is_form_data=False):
|
||||||
|
"""Make HTTP request using urllib"""
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, method=method)
|
||||||
|
for key, value in headers.items():
|
||||||
|
req.add_header(key, value)
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
if is_form_data:
|
||||||
|
# For form data (application/x-www-form-urlencoded)
|
||||||
|
data_bytes = urllib.parse.urlencode(data).encode('utf-8')
|
||||||
|
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
|
else:
|
||||||
|
# For JSON data
|
||||||
|
req.add_header('Content-Type', 'application/json')
|
||||||
|
data_bytes = json.dumps(data).encode('utf-8')
|
||||||
|
req.data = data_bytes
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
response_data = response.read().decode('utf-8')
|
||||||
|
return response.status, response_data
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.read().decode('utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
return 0, str(e)
|
||||||
|
|
||||||
|
def login():
|
||||||
|
"""Login and get JWT token"""
|
||||||
|
print("🔐 Logging in...")
|
||||||
|
login_data = {
|
||||||
|
"username": TEST_EMAIL,
|
||||||
|
"password": TEST_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
# Login endpoint expects form data, not JSON
|
||||||
|
status, response = make_request("POST", f"{BASE_URL}/auth/login", data=login_data, is_form_data=True)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
data = json.loads(response)
|
||||||
|
token = data.get("access_token")
|
||||||
|
if token:
|
||||||
|
print(f"✅ Login successful")
|
||||||
|
return token
|
||||||
|
else:
|
||||||
|
print(f"❌ No access_token in response")
|
||||||
|
print(f"Response: {response}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print(f"❌ Login failed with status {status}")
|
||||||
|
print(f"Response: {response}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_issue_176_organization_switch(token):
|
||||||
|
"""Test Issue #176: PATCH /api/v1/users/me/active-organization endpoint"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("🧪 Testing Issue #176: Organization Switch (500 Error Fix)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
# Step 1: Get current user info
|
||||||
|
print("\n1. Getting current user info...")
|
||||||
|
status, response = make_request("GET", f"{BASE_URL}/users/me", headers=headers)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
user_data = json.loads(response)
|
||||||
|
current_scope_id = user_data.get("scope_id")
|
||||||
|
print(f" Current scope_id: {current_scope_id}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to get user info: {status}")
|
||||||
|
print(f" Response: {response}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 2: Try to switch to null (personal mode)
|
||||||
|
print("\n2. Testing switch to null (personal mode)...")
|
||||||
|
switch_data = {"organization_id": None}
|
||||||
|
status, response = make_request("PATCH", f"{BASE_URL}/users/me/active-organization",
|
||||||
|
data=switch_data, headers=headers)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
print(f"✅ Switch to null successful (200 OK)")
|
||||||
|
result = json.loads(response)
|
||||||
|
print(f" New scope_id: {result.get('scope_id')}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Switch to null failed with status {status}")
|
||||||
|
print(f" Response: {response}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 3: Get organizations to test switching
|
||||||
|
print("\n3. Checking user's organizations...")
|
||||||
|
status, response = make_request("GET", f"{BASE_URL}/organizations/my", headers=headers)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
orgs = json.loads(response)
|
||||||
|
if orgs and len(orgs) > 0:
|
||||||
|
org_id = orgs[0].get("id")
|
||||||
|
print(f" Found organization with ID: {org_id}")
|
||||||
|
|
||||||
|
# Try to switch to this organization
|
||||||
|
switch_data = {"organization_id": org_id}
|
||||||
|
status, response = make_request("PATCH", f"{BASE_URL}/users/me/active-organization",
|
||||||
|
data=switch_data, headers=headers)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
print(f"✅ Switch to organization successful (200 OK)")
|
||||||
|
result = json.loads(response)
|
||||||
|
print(f" New scope_id: {result.get('scope_id')}")
|
||||||
|
elif status == 403:
|
||||||
|
print(f"⚠️ Switch to organization failed with 403 (not a member)")
|
||||||
|
print(f" This is expected if user is not a member of this org")
|
||||||
|
else:
|
||||||
|
print(f"❌ Switch to organization failed with status {status}")
|
||||||
|
print(f" Response: {response}")
|
||||||
|
# Don't fail the test if we can't switch to org (might not be a member)
|
||||||
|
else:
|
||||||
|
print(" No organizations found for user, skipping organization switch test")
|
||||||
|
else:
|
||||||
|
print(f" Could not fetch organizations: {status}")
|
||||||
|
|
||||||
|
# Step 4: Verify final state
|
||||||
|
print("\n4. Verifying final user state...")
|
||||||
|
status, response = make_request("GET", f"{BASE_URL}/users/me", headers=headers)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
user_data = json.loads(response)
|
||||||
|
final_scope_id = user_data.get("scope_id")
|
||||||
|
print(f" Final scope_id: {final_scope_id}")
|
||||||
|
print(f"✅ Issue #176 test completed successfully!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to verify user info: {status}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_issue_177_garage_dynamic_data(token):
|
||||||
|
"""Test Issue #177: GET /api/v1/assets/vehicles with dynamic data fields"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("🧪 Testing Issue #177: Garage Dynamic Data")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
# Get user's vehicles
|
||||||
|
print("\n1. Fetching user's vehicles...")
|
||||||
|
status, response = make_request("GET", f"{BASE_URL}/assets/vehicles", headers=headers)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
vehicles = json.loads(response)
|
||||||
|
print(f" Found {len(vehicles)} vehicles")
|
||||||
|
|
||||||
|
if len(vehicles) == 0:
|
||||||
|
print(" No vehicles found, but endpoint works correctly")
|
||||||
|
print(" ✅ GET /api/v1/assets/vehicles endpoint is working (200 OK)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check first vehicle
|
||||||
|
first_vehicle = vehicles[0]
|
||||||
|
print(f"\n2. Checking first vehicle:")
|
||||||
|
print(f" Vehicle ID: {first_vehicle.get('id')}")
|
||||||
|
print(f" License plate: {first_vehicle.get('license_plate')}")
|
||||||
|
|
||||||
|
# Check for profile_completion_percentage field
|
||||||
|
profile_completion = first_vehicle.get("profile_completion_percentage")
|
||||||
|
if profile_completion is not None:
|
||||||
|
print(f" ✅ profile_completion_percentage field found: {profile_completion}%")
|
||||||
|
else:
|
||||||
|
print(f" ❌ profile_completion_percentage field MISSING!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check all fields
|
||||||
|
print(f"\n3. Available fields in vehicle response:")
|
||||||
|
for key in first_vehicle.keys():
|
||||||
|
print(f" - {key}")
|
||||||
|
|
||||||
|
# Check for data_status field
|
||||||
|
if "data_status" in first_vehicle:
|
||||||
|
print(f" ✅ data_status field found: {first_vehicle['data_status']}")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ data_status field not in response")
|
||||||
|
# Check if it might be in a nested structure
|
||||||
|
for key, value in first_vehicle.items():
|
||||||
|
if isinstance(value, dict) and "data_status" in value:
|
||||||
|
print(f" ✅ data_status found in nested {key}: {value['data_status']}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
required_fields = ["id", "status", "is_verified", "created_at"]
|
||||||
|
missing_fields = []
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in first_vehicle:
|
||||||
|
missing_fields.append(field)
|
||||||
|
|
||||||
|
if missing_fields:
|
||||||
|
print(f" ❌ Missing required fields: {missing_fields}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f" ✅ All required fields present")
|
||||||
|
|
||||||
|
print(f"\n✅ Issue #177 test completed successfully!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to get vehicles: {status}")
|
||||||
|
print(f" Response: {response}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main test function"""
|
||||||
|
print("🚀 Starting Internal QA Tests for Issues #176 and #177")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Login
|
||||||
|
token = login()
|
||||||
|
if not token:
|
||||||
|
print("❌ Cannot proceed without authentication token")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Test Issue #176
|
||||||
|
issue_176_passed = test_issue_176_organization_switch(token)
|
||||||
|
|
||||||
|
# Test Issue #177
|
||||||
|
issue_177_passed = test_issue_177_garage_dynamic_data(token)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("📊 TEST SUMMARY")
|
||||||
|
print("="*60)
|
||||||
|
print(f"Issue #176 (Organization Switch): {'✅ PASSED' if issue_176_passed else '❌ FAILED'}")
|
||||||
|
print(f"Issue #177 (Garage Dynamic Data): {'✅ PASSED' if issue_177_passed else '❌ FAILED'}")
|
||||||
|
|
||||||
|
overall_passed = issue_176_passed and issue_177_passed
|
||||||
|
print(f"\nOverall Result: {'✅ ALL TESTS PASSED' if overall_passed else '❌ SOME TESTS FAILED'}")
|
||||||
|
|
||||||
|
return overall_passed
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n⚠️ Test interrupted by user")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Unexpected error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
28
backend/get_token_for_user_29.py
Normal file
28
backend/get_token_for_user_29.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import asyncio
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.core.security import create_tokens
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
result = await db.execute(select(User).where(User.id == 29))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user:
|
||||||
|
print("User 29 not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"User found: {user.email}, Scope ID: {user.scope_id}")
|
||||||
|
|
||||||
|
token_payload = {
|
||||||
|
"sub": str(user.id),
|
||||||
|
"role": user.role.value if hasattr(user.role, 'value') else user.role,
|
||||||
|
"rank": 10,
|
||||||
|
"scope_level": user.scope_level.value if hasattr(user.scope_level, 'value') else (user.scope_level or "individual"),
|
||||||
|
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||||
|
}
|
||||||
|
access_token, refresh_token = create_tokens(data=token_payload)
|
||||||
|
print(f"TOKEN={access_token}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
31
backend/raw_data_dump.py
Normal file
31
backend/raw_data_dump.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import asyncio
|
||||||
|
from sqlalchemy import text
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
print("--- TASK 1: BRANCHES (GARAGES) ---")
|
||||||
|
try:
|
||||||
|
result = await session.execute(text("SELECT id, organization_id, name FROM fleet.branches;"))
|
||||||
|
branches = result.fetchall()
|
||||||
|
print("| id | organization_id | name |")
|
||||||
|
print("|---|---|---|")
|
||||||
|
for b in branches:
|
||||||
|
print(f"| {b.id} | {b.organization_id} | {b.name} |")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error querying fleet.branches: {e}")
|
||||||
|
|
||||||
|
print("\n--- TASK 2: ASSETS (VEHICLES) ---")
|
||||||
|
try:
|
||||||
|
# We will query all assets and print the relevant columns.
|
||||||
|
result = await session.execute(text("SELECT id, license_plate, owner_person_id, owner_org_id, branch_id FROM vehicle.assets LIMIT 50;"))
|
||||||
|
assets = result.fetchall()
|
||||||
|
print("| id | license_plate | owner_person_id | owner_org_id | branch_id |")
|
||||||
|
print("|---|---|---|---|---|")
|
||||||
|
for a in assets:
|
||||||
|
print(f"| {a.id} | {a.license_plate} | {a.owner_person_id} | {a.owner_org_id} | {a.branch_id} |")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error querying vehicle.assets: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
116
backend/test_schema_changes.py
Normal file
116
backend/test_schema_changes.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Ellenőrzi, hogy a #179-es issue séma változtatásai sikeresen alkalmazva lettek-e.
|
||||||
|
- branch_id és relocation_performed oszlopok az assets táblában
|
||||||
|
- verified_purchase_date oszlop az asset_financials táblában
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@postgres:5432/service_finder"
|
||||||
|
|
||||||
|
async def check_columns():
|
||||||
|
"""Ellenőrzi a hiányzó oszlopokat."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
async with async_session() as session:
|
||||||
|
# 1. assets tábla oszlopai
|
||||||
|
result = await session.execute(text("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'vehicle' AND table_name = 'assets'
|
||||||
|
AND column_name IN ('branch_id', 'relocation_performed')
|
||||||
|
ORDER BY column_name;
|
||||||
|
"""))
|
||||||
|
assets_cols = {row[0]: row for row in result.fetchall()}
|
||||||
|
|
||||||
|
# 2. asset_financials tábla oszlopai
|
||||||
|
result = await session.execute(text("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'vehicle' AND table_name = 'asset_financials'
|
||||||
|
AND column_name = 'verified_purchase_date';
|
||||||
|
"""))
|
||||||
|
financials_cols = {row[0]: row for row in result.fetchall()}
|
||||||
|
|
||||||
|
print("=== #179 SCHEMA ELLENŐRZÉS ===")
|
||||||
|
print("1. vehicle.assets tábla:")
|
||||||
|
if 'branch_id' in assets_cols:
|
||||||
|
col = assets_cols['branch_id']
|
||||||
|
print(f" ✅ branch_id: {col[1]} (nullable: {col[2]})")
|
||||||
|
else:
|
||||||
|
print(" ❌ branch_id oszlop HIÁNYZIK!")
|
||||||
|
|
||||||
|
if 'relocation_performed' in assets_cols:
|
||||||
|
col = assets_cols['relocation_performed']
|
||||||
|
print(f" ✅ relocation_performed: {col[1]} (nullable: {col[2]})")
|
||||||
|
else:
|
||||||
|
print(" ❌ relocation_performed oszlop HIÁNYZIK!")
|
||||||
|
|
||||||
|
print("\n2. vehicle.asset_financials tábla:")
|
||||||
|
if 'verified_purchase_date' in financials_cols:
|
||||||
|
col = financials_cols['verified_purchase_date']
|
||||||
|
print(f" ✅ verified_purchase_date: {col[1]} (nullable: {col[2]})")
|
||||||
|
else:
|
||||||
|
print(" ❌ verified_purchase_date oszlop HIÁNYZIK!")
|
||||||
|
|
||||||
|
# 3. Foreign key ellenőrzés (opcionális)
|
||||||
|
result = await session.execute(text("""
|
||||||
|
SELECT tc.constraint_name, tc.constraint_type
|
||||||
|
FROM information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.key_column_usage kcu
|
||||||
|
ON tc.constraint_name = kcu.constraint_name
|
||||||
|
WHERE tc.table_schema = 'vehicle' AND tc.table_name = 'assets'
|
||||||
|
AND kcu.column_name = 'branch_id'
|
||||||
|
AND tc.constraint_type = 'FOREIGN KEY';
|
||||||
|
"""))
|
||||||
|
fk = result.fetchone()
|
||||||
|
if fk:
|
||||||
|
print(f"\n3. Foreign key meglétének ellenőrzése: ✅ {fk[0]}")
|
||||||
|
else:
|
||||||
|
print("\n3. Foreign key meglétének ellenőrzése: ⚠️ Nincs foreign key constraint (lehet, hogy nem kötelező)")
|
||||||
|
|
||||||
|
# 4. Default érték ellenőrzés relocation_performed-re
|
||||||
|
result = await session.execute(text("""
|
||||||
|
SELECT column_default
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'vehicle' AND table_name = 'assets'
|
||||||
|
AND column_name = 'relocation_performed';
|
||||||
|
"""))
|
||||||
|
default = result.scalar()
|
||||||
|
if default and 'false' in default.lower():
|
||||||
|
print(f"4. Default érték relocation_performed: ✅ {default}")
|
||||||
|
else:
|
||||||
|
print(f"4. Default érték relocation_performed: ⚠️ {default}")
|
||||||
|
|
||||||
|
# Összegzés
|
||||||
|
missing = []
|
||||||
|
if 'branch_id' not in assets_cols:
|
||||||
|
missing.append('branch_id')
|
||||||
|
if 'relocation_performed' not in assets_cols:
|
||||||
|
missing.append('relocation_performed')
|
||||||
|
if 'verified_purchase_date' not in financials_cols:
|
||||||
|
missing.append('verified_purchase_date')
|
||||||
|
|
||||||
|
if not missing:
|
||||||
|
print("\n🎉 ÖSSZES SÉMA VÁLTOZTATÁS SIKERESEN ALKALMAZVA!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"\n❌ HIÁNYZÓ OSZLOPOK: {', '.join(missing)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
try:
|
||||||
|
success = await check_columns()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Hiba történt: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
61
docs/masterbook_2.0.1/cleanup_audit_root.md
Normal file
61
docs/masterbook_2.0.1/cleanup_audit_root.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Gyökérkönyvtár Audit és Tisztítási Terv (Root Directory Cleanup Audit)
|
||||||
|
|
||||||
|
A "Spring Cleaning" első fázisa keretében átvizsgáltuk a projekt gyökérkönyvtárában található összes fájlt. Az alábbi lista a fájlok kategorizálását tartalmazza. Még semmilyen törlés vagy áthelyezés nem történt. Kérjük, hagyd jóvá a lenti felosztást a végrehajtás előtt.
|
||||||
|
|
||||||
|
## 🟢 Helyén marad és kell (Keep in place)
|
||||||
|
- `.env`: A projekt fő környezeti változóit és titkos konfigurációit tartalmazza.
|
||||||
|
- `.gitignore`: A Git verziókövetésből kizárandó fájlok és mappák listája.
|
||||||
|
- `.roomodes`: A Roo kódoló AI asszisztens egyedi profiljainak és eszközeinek definíciója.
|
||||||
|
- `docker-compose.yml`: A teljes mikroszolgáltatásos architektúra (Docker) leírása.
|
||||||
|
- `readme.md`: A projekt alapvető leírását és telepítési/indítási útmutatóját tartalmazza.
|
||||||
|
- `service_finder.code-workspace`: A VS Code projekt munkaterületének beállítási fájlja.
|
||||||
|
- `sf_gitea.sh`: A beépített Gitea projektmenedzsment-eszköz indítását és menedzselését végző shell script.
|
||||||
|
- `sf_run.sh`: A projekt Docker-alapú szolgáltatásainak elindítását leegyszerűsítő szkript.
|
||||||
|
|
||||||
|
## 🟡 Kell, de át kell helyezni (Keep but move)
|
||||||
|
- `backup_manager.sh`: Biztonsági mentéseket kezelő szkript. Át kell helyezni a `.roo/scripts/` vagy `/scripts/` mappába a többi operációs szkript mellé.
|
||||||
|
- `MILESTONE_8_GAMIFICATION_PRO.md`: Gamifikációs projektterv. Át kell helyezni a `docs/masterbook_2.0.1/` mappába a többi dokumentációhoz.
|
||||||
|
- `Roo code_utasitas.md`: Magyar nyelvű Roo AI utasítások. A `docs/` vagy `.roo/` mappába való a gyökér helyett.
|
||||||
|
|
||||||
|
## 🔴 Nem kell, mehet az archívumba (Archive/Delete)
|
||||||
|
- `=`: Véletlenül generált, értelmetlen nevű output fájl.
|
||||||
|
- `0`: Szintén egy véletlenül létrejött fájl, valószínűleg egy teszt kimenete.
|
||||||
|
- `add_categories.py`: Egyszeri adatbázis-feltöltő szkript a kategóriák inicializálásához.
|
||||||
|
- `audit_report_robots_local.md`: Régi, már elavult robot-audit jelentés.
|
||||||
|
- `audit_report_vehicle_robots.md`: Korábbi, feleslegessé vált járműrobot audit napló.
|
||||||
|
- `classify_workers.py`: Ideiglenes szkript a teszt-munkások kategorizálására.
|
||||||
|
- `complete_ailogs.py`: Az AI logok kiegészítésére szolgáló eldobható script.
|
||||||
|
- `create_diff.py`: Kód-összehasonlításhoz használt, egyszeri segédszkript.
|
||||||
|
- `create_integration_session.py`: Integrációs tesztek munkamenetének gyors legenerálására használt script.
|
||||||
|
- `create_sandbox_user.py`: Egyszeri, eldobható homokozó-felhasználót generáló kód.
|
||||||
|
- `create_test_identity.py`: A "Dual Entity" identity modell tesztelésére szánt egyszeri script.
|
||||||
|
- `create_test_user_simple.py`: Gyors, lokális tesztfelhasználót létrehozó segédkód.
|
||||||
|
- `database_check_test.txt`: Adatbázis-kapcsolat tesztjének szöveges eredményfájlja.
|
||||||
|
- `db_audit_report.csv`: Korábbi adatbázis-elemzésből származó táblázatos export.
|
||||||
|
- `fix_classification.py`: Egy korábbi adatbázis hibát javító, ma már felesleges migrációs szkript.
|
||||||
|
- `fix_schema_refs.py`: Séma-hivatkozások javítását végző, lejárt szükségességű script.
|
||||||
|
- `full_schema_backup_2026-02-14.sql`: Régi, februári adatbázis biztonsági mentés.
|
||||||
|
- `git init`: Egy véletlenül fájlként létrehozott "git init" parancs eredménye.
|
||||||
|
- `gitea_audit_report.md`: Elavult, régebbi Gitea állapotjelentés.
|
||||||
|
- `gitea_body.md`: Valószínűleg egy Gitea API hívás tesztelésénél hátramaradt body payload fájl.
|
||||||
|
- `manual_migration_summary.md`: Egy régi kézi adatbázis módosítás dokumentációja.
|
||||||
|
- `rdw_probe.py`: A holland rendszám API (RDW) működését vizsgáló kísérleti script.
|
||||||
|
- `reset_test_user_password.py`: Tesztfelhasználó jelszavának nullázására használt segédkód.
|
||||||
|
- `schema_dump.sql`: Adatbázis sémájának ideiglenes mentése.
|
||||||
|
- `seed_discovery.py`: A GB Discovery táblák adatokkal való feltöltésére írt egyszeri script.
|
||||||
|
- `test_catalog_simple.py`: A járműkatalógust lokálisan, terminálból tesztelő fájl.
|
||||||
|
- `test_catalog_verification_v2.py`: A jármű-ellenőrzési folyamat második verziójának ideiglenes tesztje.
|
||||||
|
- `test_catalog_verification.py`: A katalógus validációt vizsgáló korábbi tesztkód.
|
||||||
|
- `test_draft_vehicle.py`: A `DRAFT` járműstátusz logikáját kipróbáló szkript.
|
||||||
|
- `test_final_verification.py`: A jármű-katalógus végső jóváhagyási lépését ellenőrző kód.
|
||||||
|
- `test_integration.py`: Egy átfogó integrációs teszt kísérleti fájlja.
|
||||||
|
- `test_mailpit.py`: A Mailpit e-mail elfogó szolgáltatást vizsgáló teszt.
|
||||||
|
- `test_r0_spider.py`: A 0-s adatgyűjtő robot (Spider) működésének teszt szkriptje.
|
||||||
|
- `test_registration_smtp.py`: E-mail alapú regisztrációs folyamat validálására írt fájl.
|
||||||
|
- `tree.txt`: Egy korábbi mappa-struktúra listázásának (tree) kimenete.
|
||||||
|
- `update_env.py`: Az `.env` fájl módosítására írt ideiglenes script.
|
||||||
|
- `update_ledger.awk`: Főkönyvi vagy log fájl manipulációra szolgáló AWK script.
|
||||||
|
- `update_ledger.py`: A pénzügyi főkönyvi folyamatot tesztelő Python kód.
|
||||||
|
- `vehicle.modelfile`: Ollama AI modell beállítási fájl a járműfelismeréshez.
|
||||||
|
- `verify_financial_truth_simple.py`: Pénzügyi modul működését validáló eldobható script.
|
||||||
|
- `verify_financial_truth.py`: A Triple Wallet gazdasági motor mélyebb lokális tesztje.
|
||||||
35
docs/masterbook_2.0.1/cleanup_audit_subdirs.md
Normal file
35
docs/masterbook_2.0.1/cleanup_audit_subdirs.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Alkönyvtárak Audit és Tisztítási Terv (Subdirectory Cleanup Audit)
|
||||||
|
|
||||||
|
A "Spring Cleaning" második fázisa keretében átvizsgáltuk a `/backend` és `/frontend` könyvtárakat, különös tekintettel az ideiglenes fájlokra, tesztszkriptekre és nem használt komponensekre. Az alábbi lista a fájlok kategorizálását tartalmazza. Még semmilyen törlés vagy áthelyezés nem történt. Kérjük, hagyd jóvá a lenti felosztást a végrehajtás előtt.
|
||||||
|
|
||||||
|
## 🟢 Helyén marad és kell (Keep in place)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- `/backend/app/tests/e2e/*`: Az End-to-End integrációs tesztek mappája. Ezek a kódok szükségesek a CI/CD pipeline-hoz és a rendszeres ellenőrzésekhez (pl. `test_robot.py`, `test_organization_flow.py`).
|
||||||
|
- `/backend/app/tests/test_admin_audit_gitea.py` és variánsai: A Gitea és adminisztrációs audit logikát validáló tesztek.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- `/frontend/tests/e2e/frontend-flow.spec.js`: A Playwright alapú e2e tesztelés specifikációja, szükséges a UI automatizált teszteléséhez.
|
||||||
|
|
||||||
|
## 🟡 Kell, de át kell helyezni (Keep but move)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- `/backend/test_registration.py`, `/backend/test_registration2.py`, `/backend/sendgrid_live_test.py`: Gyökérmappában lévő (backend/) tesztfájlok, amiket be kell mozgatni a `/backend/app/tests/` könyvtárba.
|
||||||
|
- `/backend/test_catalog_verification.py`, `/backend/test_catalog_verification_v2.py`, `/backend/test_catalog_simple.py`, `/backend/test_final_verification.py`: Katalógus ellenőrző tesztek, amiknek szintén a `/backend/app/tests/` alatt a helyük.
|
||||||
|
- `/backend/create_test_user.py`, `/backend/create_test_user_fixed.py`, `/backend/create_test_user_final.py`: Ezeket a segédszkripteket egy új `/backend/scripts/` vagy `/backend/app/scripts/` mappába kell helyezni.
|
||||||
|
- `/backend/reset_test_user_password.py`: Tesztadatokat manipuláló script, helye a `/backend/scripts/` mappában van.
|
||||||
|
|
||||||
|
## 🔴 Nem kell, mehet az archívumba (Archive/Delete)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- `/backend/app/test_billing_engine.py`: Valószínűleg elavult, ideiglenes tesztfájl, ami nem a hivatalos tesztmappában van.
|
||||||
|
- `/backend/app/test_hierarchical.py`: Szintén egy eldobható, ideiglenes tesztszkript.
|
||||||
|
- `/backend/archive_v1_scripts/test_config_service.py`: V1-es archív script, már nem releváns.
|
||||||
|
- `/backend/test_asset_schema.py`: Ideiglenes sématesztelő script a backend gyökerében.
|
||||||
|
- `/backend/temp`: Üres vagy átmeneti fájlokat tartalmazó mappa, törölhető.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- `/frontend/tests/automated_flow_test.js`: Régi vagy kísérleti tesztfájl, a Playwright vette át a helyét (`frontend-flow.spec.js`).
|
||||||
|
- `/frontend/admin/test-structure.sh`: Adminisztrációs felülethez tartozó eldobható vagy ideiglenes tesztszkript.
|
||||||
|
- `/frontend/admin/.nuxt/`: Átmeneti build és cache mappák, amiket a fejlesztői szerver generál. Nem kell verziókövetni, és törölhetők (újragenerálódnak).
|
||||||
|
- `/frontend/test-results/`: Tesztfuttatások kimeneti mappája (pl. Playwright riportok), nem szükséges megtartani a forráskódban.
|
||||||
45
docs/masterbook_2.0.1/deep_analysis_v201.md
Normal file
45
docs/masterbook_2.0.1/deep_analysis_v201.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Masterbook 2.0.1: Backend - Frontend UI Szakadék Elemzés
|
||||||
|
|
||||||
|
**Dátum:** 2026. március 29.
|
||||||
|
**Készítette:** Enterprise Solutions Architect
|
||||||
|
**Referencia:** Issue #172 Mélyelemzés
|
||||||
|
|
||||||
|
## 1. Vezetői Összefoglaló (A Brutális Valóság)
|
||||||
|
|
||||||
|
A mai nap folyamán a Backend rendszermag (SQLAlchemy + Database + API) sikeresen megkapta a Masterbook 2.0.1 legfejlettebb vállalati funkcióit:
|
||||||
|
1. **`data_status` oszlop** a `vehicle.assets` táblában (DRAFT, ACTIVE, stb. státuszokhoz).
|
||||||
|
2. **`vehicle_transfer_requests` tábla** a tulajdonosváltási (Owner transfer) forgatókönyvekhez (rendszám igénylés/átvétel/elutasítás).
|
||||||
|
3. **`system_data_completion_weights` tábla és dinamikus `completion_percentage`** a profil kitöltöttség mérésére (Gamification és Trust score alapja).
|
||||||
|
|
||||||
|
**A Frontend azonban jelenleg "vak" és teljesen leválasztott ezekről a mély, vállalati szintű (Enterprise) funkciókról.**
|
||||||
|
A Vue.js komponensek, mint a `Dashboard.vue`, `VehicleCard.vue`, `VehicleShowcase.vue` vagy `AddVehicle.vue` vagy hardkódolt értékekkel (vagy egyszerű fallbacks-ekkel) dolgoznak, vagy egyáltalán nem is jelenítik meg a Backend által szolgáltatott adatokat. Ráadásul az alapvető navigációt biztosító profilt váltó rendszerek (Profile/Organization Switcher) 500-as Internal Server Errort dobnak bizonyos esetekben, ami blokkolja a Garage funkciók rendes tesztelését.
|
||||||
|
|
||||||
|
## 2. Részletes Elemzés (Mélyfúrás a kódban)
|
||||||
|
|
||||||
|
### 2.1. A Dinamikus Profil Kitöltöttség (Completion Percentage) és Data Status Hiánya
|
||||||
|
**A Backend oldalon:**
|
||||||
|
A rendszer a `system_data_completion_weights` alapján súlyozza a megadott adatokat, és API válaszban küldené a dinamikusan kalkulált `completion_percentage` értéket és az adat minőségét/státuszát (`data_status`).
|
||||||
|
**A Frontend oldalon:**
|
||||||
|
- A `frontend/src/components/garage/VehicleCard.vue` fájlban jelenleg egy hardkódolt vagy hiányos fall-back szerepel:
|
||||||
|
```vue
|
||||||
|
<div class="text-xs text-gray-600 mb-1">Profile: {{ vehicle.profile_completion_percentage || 0 }}% Complete</div>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-yellow-500 h-2 rounded-full" :style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
- A `vehicle.status` ugyan ki van vezetve (pl. `'draft'`), de az új `data_status` szintű bontás (enum: DRAFT, DISCOVERED, ENRICHED, ACTIVE, ARCHIVED stb.) vizualizálása nem teljes körű vagy nincs a helyes backend mezőre drótozva.
|
||||||
|
|
||||||
|
### 2.2. A Jármű Átruházás (Vehicle Transfer Requests) teljes hiánya a UI-on
|
||||||
|
**A Backend oldalon:**
|
||||||
|
Teljes életciklus kezelés (Scenario A, B, C) készült a rendszám alapján történő igénylésre, konfliktuskezelésre (ha már valakié az autó), és tulajdonosváltás lebonyolítására.
|
||||||
|
**A Frontend oldalon:**
|
||||||
|
- A `frontend/src/views/AddVehicle.vue` csak egy egyszerű form (`model_name`, `plate`, `vin`, `current_odo`), amely a `POST /assets/vehicles` endpointot hívja.
|
||||||
|
- Nincs UI ág arra az esetre, ha a API 409 Conflict-ot adna vissza (már létezik a jármű).
|
||||||
|
- Nincs "Transfer Request" nézet, ahol a felhasználó láthatná a bejövő és kimenő jármű igényléseit, és ahol jóváhagyhatná/elutasíthatná azokat.
|
||||||
|
|
||||||
|
### 2.3. A Blokkoló Hiba: 500-as hiba a Profile/Organization Switcher-ben
|
||||||
|
Ahhoz, hogy a B2B és B2C (Private Garage vs. Corporate Fleet) nézeteket és járműveket érdemben tesztelni lehessen, a fiókváltás kritikus. Jelenleg a Profile Select / Org Switcher backend/frontend integrációja bizonyos API hívásoknál 500-as Internal Server Errorra fut, ami lehetetlenné teszi a különböző szerepkörök (Owner vs. Member, Private vs. Corporate) tiszta elkülönítését és tesztelését a Garage nézetben.
|
||||||
|
|
||||||
|
## 3. Következtetés
|
||||||
|
|
||||||
|
Jelenleg egy "Ferrari motoros Trabantot" építettünk. A Backend enterprise szinten kezeli az állapottérgépet (State Machine), a minőségi pontozást és az eszköztulajdonosi konfliktusokat, míg a Frontend egy v1-es, "MVP" (Minimum Viable Product) szintű UI-t használ. Ezt a szakadékot azonnal, három célzott Issue (Gitea jegy) formájában kell áthidalnunk.
|
||||||
200
docs/masterbook_2.0.1/garage_hierarchy.md
Normal file
200
docs/masterbook_2.0.1/garage_hierarchy.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# 🏢 Garage-Centric Hierarchy Audit Report
|
||||||
|
|
||||||
|
**Audit Date**: 2026-03-29
|
||||||
|
**Issue**: #179 - SYSTEM ARCHITECT - CODE & SCHEMA AUDIT
|
||||||
|
**Auditor**: Fast Coder (Core Developer)
|
||||||
|
|
||||||
|
## 📋 Executive Summary
|
||||||
|
|
||||||
|
This audit verifies the implementation of the "Masterbook 2.0.1 Hierarchy": **User → Org (Private/Corp) → Branch → Garage → Vehicle**. The audit reveals that the core hierarchy is partially implemented with some gaps that need to be addressed.
|
||||||
|
|
||||||
|
## 🔍 Current Implementation Status
|
||||||
|
|
||||||
|
### ✅ **IMPLEMENTED CORRECTLY**
|
||||||
|
|
||||||
|
#### 1. Registration Step 2 - Private Organization Creation
|
||||||
|
- **Location**: `backend/app/services/auth_service.py` - `complete_kyc()` method
|
||||||
|
- **Status**: ✅ **FULLY IMPLEMENTED**
|
||||||
|
- **Details**: When a user completes KYC (Registration Step 2), the system:
|
||||||
|
- Creates an `Organization` with `OrgType.individual`
|
||||||
|
- Sets `user.scope_id = str(new_org.id)`
|
||||||
|
- Creates a default `Branch` with `name="Home Base"` and `is_main=True`
|
||||||
|
- Creates `OrganizationMember` with `role="OWNER"`
|
||||||
|
- **Code Reference**: Lines 159-191 in `auth_service.py`
|
||||||
|
|
||||||
|
#### 2. Organization Model
|
||||||
|
- **Location**: `backend/app/models/marketplace/organization.py`
|
||||||
|
- **Status**: ✅ **COMPLETE**
|
||||||
|
- **Schema**: `fleet.organizations`
|
||||||
|
- **Key Fields**: `id`, `full_name`, `org_type`, `owner_id`, `status`, `is_active`
|
||||||
|
- **Relationships**: Has `branches` relationship to `Branch` model
|
||||||
|
|
||||||
|
#### 3. Branch Model (Acting as Garage)
|
||||||
|
- **Location**: `backend/app/models/marketplace/organization.py` - `Branch` class
|
||||||
|
- **Status**: ✅ **COMPLETE**
|
||||||
|
- **Schema**: `fleet.branches`
|
||||||
|
- **Key Fields**: `id` (UUID), `organization_id`, `name`, `is_main`, `address_id`
|
||||||
|
- **Note**: No dedicated "Garage" model exists - Branch serves as the Garage container
|
||||||
|
|
||||||
|
#### 4. Asset Model
|
||||||
|
- **Location**: `backend/app/models/vehicle/asset.py` - `Asset` class
|
||||||
|
- **Status**: ✅ **COMPLETE**
|
||||||
|
- **Schema**: `vehicle.assets`
|
||||||
|
- **Key Fields**: `id` (UUID), `vin`, `license_plate`, `current_organization_id`, `owner_org_id`
|
||||||
|
|
||||||
|
### ⚠️ **PARTIALLY IMPLEMENTED / GAPS IDENTIFIED**
|
||||||
|
|
||||||
|
#### 1. Asset to Branch/Garage Linkage
|
||||||
|
- **Status**: ❌ **MISSING**
|
||||||
|
- **Issue**: Assets are linked to Organizations via `current_organization_id` and `owner_org_id`, but there's no direct `branch_id` or `garage_id` field to specify which Branch/Garage the asset belongs to.
|
||||||
|
- **Current**: Assets use `AssetAssignment` model for many-to-many relationship with Organizations
|
||||||
|
- **Required**: Add `branch_id` field to Asset model
|
||||||
|
|
||||||
|
#### 2. Verified Purchase Date Field
|
||||||
|
- **Status**: ⚠️ **PARTIAL**
|
||||||
|
- **Current Fields**:
|
||||||
|
- `first_registration_date` in Asset model (line 48)
|
||||||
|
- `activation_date` in AssetFinancials model (line 146)
|
||||||
|
- **Missing**: Explicit `verified_purchase_date` field for cost cut-off logic
|
||||||
|
- **Required**: Add `verified_purchase_date` to AssetFinancials model
|
||||||
|
|
||||||
|
#### 3. Relocation Performed Flag
|
||||||
|
- **Status**: ❌ **MISSING**
|
||||||
|
- **Issue**: No `relocation_performed` flag for one-time relocation logic
|
||||||
|
- **Required**: Add boolean `relocation_performed` field to Asset model
|
||||||
|
|
||||||
|
## 🗺️ Hierarchy Mapping
|
||||||
|
|
||||||
|
```
|
||||||
|
User (identity.users)
|
||||||
|
↓
|
||||||
|
Organization (fleet.organizations) ← Private Org created during KYC
|
||||||
|
↓
|
||||||
|
Branch (fleet.branches) ← Acts as "Garage" container
|
||||||
|
↓
|
||||||
|
Asset (vehicle.assets) ← Missing direct branch_id linkage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Database Schema Analysis
|
||||||
|
|
||||||
|
### Current Asset-Organization Relationship
|
||||||
|
```sql
|
||||||
|
-- Current linkage (via AssetAssignment)
|
||||||
|
asset_assignments (fleet schema)
|
||||||
|
asset_id → vehicle.assets.id
|
||||||
|
organization_id → fleet.organizations.id
|
||||||
|
|
||||||
|
-- Direct fields in Asset
|
||||||
|
assets (vehicle schema)
|
||||||
|
current_organization_id → fleet.organizations.id
|
||||||
|
owner_org_id → fleet.organizations.id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Branch Linkage
|
||||||
|
```sql
|
||||||
|
-- PROPOSED ADDITION to Asset model
|
||||||
|
ALTER TABLE vehicle.assets
|
||||||
|
ADD COLUMN branch_id UUID REFERENCES fleet.branches(id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Required Code Modifications
|
||||||
|
|
||||||
|
### 1. Asset Model Updates (`backend/app/models/vehicle/asset.py`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add to Asset class (around line 64):
|
||||||
|
branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||||
|
PG_UUID(as_uuid=True),
|
||||||
|
ForeignKey("fleet.branches.id"),
|
||||||
|
nullable=True
|
||||||
|
)
|
||||||
|
relocation_performed: Mapped[bool] = mapped_column(
|
||||||
|
Boolean,
|
||||||
|
default=False,
|
||||||
|
server_default=text("false")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add relationship:
|
||||||
|
branch: Mapped[Optional["Branch"]] = relationship("Branch")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. AssetFinancials Model Updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add to AssetFinancials class:
|
||||||
|
verified_purchase_date: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Schema Updates Required
|
||||||
|
- Run `sync_engine.py` after model changes
|
||||||
|
- **DO NOT** use Alembic migrations directly per project rules
|
||||||
|
|
||||||
|
## 🧪 Test Script for Hierarchy Verification
|
||||||
|
|
||||||
|
A test script has been prepared to verify the separation logic:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Test: Create 1 Private Garage car and 1 Corporate Branch car
|
||||||
|
# Verify GET /vehicles (with active scope_id) only shows cars belonging to specific context
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Logic**:
|
||||||
|
1. Create User A with Private Organization (Org A)
|
||||||
|
2. Create User B with Corporate Organization (Org B)
|
||||||
|
3. Add Vehicle 1 to Org A's Branch
|
||||||
|
4. Add Vehicle 2 to Org B's Branch
|
||||||
|
5. Verify User A only sees Vehicle 1 when calling GET /vehicles
|
||||||
|
6. Verify User B only sees Vehicle 2 when calling GET /vehicles
|
||||||
|
|
||||||
|
## 🚨 Critical Findings
|
||||||
|
|
||||||
|
### 1. **Scope Isolation Works**
|
||||||
|
The `user.scope_id` mechanism correctly isolates data at the Organization level. When a user queries `/vehicles`, the API filters by `current_organization_id = user.scope_id`.
|
||||||
|
|
||||||
|
### 2. **Branch-Level Isolation Missing**
|
||||||
|
While Organization-level isolation exists, there's no Branch-level filtering. All assets in an Organization are visible regardless of which Branch they belong to.
|
||||||
|
|
||||||
|
### 3. **Cost Cut-off Logic Incomplete**
|
||||||
|
Without `verified_purchase_date` and `relocation_performed` fields, the historical cost tracking and one-time relocation logic cannot be properly implemented.
|
||||||
|
|
||||||
|
## 📝 Recommendations
|
||||||
|
|
||||||
|
### **HIGH PRIORITY**
|
||||||
|
1. **Add `branch_id` to Asset model** - Enable Branch/Garage level asset management
|
||||||
|
2. **Add `verified_purchase_date` to AssetFinancials** - Support cost cut-off logic
|
||||||
|
3. **Add `relocation_performed` flag to Asset** - Enable one-time relocation tracking
|
||||||
|
|
||||||
|
### **MEDIUM PRIORITY**
|
||||||
|
4. Update API endpoints to respect `branch_id` in queries
|
||||||
|
5. Enhance admin interface to show Branch assignment for assets
|
||||||
|
6. Add Branch filtering to vehicle listing endpoints
|
||||||
|
|
||||||
|
### **LOW PRIORITY**
|
||||||
|
7. Consider creating dedicated `Garage` model if Branch semantics differ significantly
|
||||||
|
8. Add Branch-level permissions for fleet managers
|
||||||
|
|
||||||
|
## 🔄 Synchronization Process
|
||||||
|
|
||||||
|
**STRICT RULE**: Apply changes ONLY via `sync_engine.py`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec roo-helper python3 /app/backend/app/scripts/sync_engine.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**DO NOT** use Alembic migrations directly. The sync engine is the primary source of truth for schema generation in Masterbook 2.0.1 architecture.
|
||||||
|
|
||||||
|
## 📈 Next Steps
|
||||||
|
|
||||||
|
1. **User Approval**: Present this "Gap Report" for approval before implementing changes
|
||||||
|
2. **Implementation**: Apply the three high-priority model modifications
|
||||||
|
3. **Sync**: Run `sync_engine.py` to update database schema
|
||||||
|
4. **Testing**: Execute the hierarchy verification test script
|
||||||
|
5. **Validation**: Confirm GET /vehicles endpoint respects scope isolation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Audit Completed**: 2026-03-29
|
||||||
|
**Next Review**: After gap implementation approval
|
||||||
188
docs/service_finder_gitea_jegy_lista.md
Normal file
188
docs/service_finder_gitea_jegy_lista.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# 📋 Service Finder Gitea Jegy Teljes Lista
|
||||||
|
|
||||||
|
*Utolsó frissítés: 2026-03-29*
|
||||||
|
*Összes jegy: 175 (35 nyitott, 140 lezárt)*
|
||||||
|
|
||||||
|
## 📊 Áttekintés
|
||||||
|
|
||||||
|
Ez a dokumentum a Service Finder rendszer összes Gitea jegyét tartalmazza, kategóriák szerint csoportosítva. A lista tartalmazza mind a nyitott, mind a lezárt jegyeket, amelyek a rendszer fejlesztési és karbantartási folyamatait dokumentálják.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 NYITOTT JEGYEK (35 db)
|
||||||
|
|
||||||
|
### V0 - Rendszertisztítás és Alapozás
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #172 | Masterbook 2.0.1 Valós Mélyelemzés (Tisz | Nyitott | V0 - Rendszertisztítás és Alapozás |
|
||||||
|
| #173 | Service Finder Rendszer Áttekintő Dokumentáció | Nyitott | - |
|
||||||
|
|
||||||
|
### Phase 1: Core Functionality Fixes
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #146 | Implement Basic Error Handling in Frontend | Nyitott | Phase 1: Core Functionality Fixes |
|
||||||
|
| #145 | Standardize API Base URL Usage in Frontend | Nyitott | Phase 1: Core Functionality Fixes |
|
||||||
|
| #142 | Implement Catalog API Endpoints | Nyitott | Phase 1: Core Functionality Fixes |
|
||||||
|
|
||||||
|
### Phase 2: Dashboard & Analytics Wiring
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #152 | Implement Historical Data (occurrence_date) | Nyitott | Phase 2: Dashboard & Analytics Wiring |
|
||||||
|
| #151 | Connect User Management Table to Real Data | Nyitott | Phase 2: Dashboard & Analytics Wiring |
|
||||||
|
| #150 | Wire Service Map with Real Provider Data | Nyitott | Phase 2: Dashboard & Analytics Wiring |
|
||||||
|
| #149 | Implement Analytics Service (TCO/km Calculator) | Nyitott | Phase 2: Dashboard & Analytics Wiring |
|
||||||
|
| #148 | Connect Gamification Components to Real Backend | Nyitott | Phase 2: Dashboard & Analytics Wiring |
|
||||||
|
| #147 | Wire Financial Dashboard to Real Financial Data | Nyitott | Phase 2: Dashboard & Analytics Wiring |
|
||||||
|
| #175 | Advanced Analytics & Reporting Engine | Nyitott | - |
|
||||||
|
|
||||||
|
### Phase 3: Advanced Features & Epic 11
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #158 | Implement Advanced Search with Filters | Nyitott | Phase 3: Advanced Features & Epic 11 |
|
||||||
|
| #157 | Add Bulk Operations (Vehicle Import, Mass Updates) | Nyitott | Phase 3: Advanced Features & Epic 11 |
|
||||||
|
| #156 | Implement Webhook and Notification System | Nyitott | Phase 3: Advanced Features & Epic 11 |
|
||||||
|
| #155 | Add Admin Control Panels (Gamification Rules) | Nyitott | Phase 3: Advanced Features & Epic 11 |
|
||||||
|
| #154 | Implement Service Booking Flow | Nyitott | Phase 3: Advanced Features & Epic 11 |
|
||||||
|
| #153 | Complete Profile Selector (Private vs Commercial) | Nyitott | Phase 3: Advanced Features & Epic 11 |
|
||||||
|
| #174 | Real-time Notification System | Nyitott | - |
|
||||||
|
|
||||||
|
### Phase 4: Testing & Deployment
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #165 | Production Deployment and Monitoring Setup | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
| #164 | CI/CD Pipeline Setup (GitHub Actions) | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
| #163 | Security Audit (Penetration Testing, Vulnerability Scan) | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
| #162 | Accessibility Audit and Fixes (WCAG 2.1 AA) | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
| #161 | Performance Optimization (Bundle Size, API Response Time) | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
| #160 | Implement Integration Tests (Playwright) | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
| #159 | Write Unit Tests for Critical Components | Nyitott | Phase 4: Testing & Deployment |
|
||||||
|
|
||||||
|
### Egyéb Nyitott Jegyek
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #140 | Connect Service Moderation Map to Backend | Nyitott | - |
|
||||||
|
| #139 | Integrate Gamification Control Panel with Backend | Nyitott | - |
|
||||||
|
| #138 | Connect Financial Dashboard Tile to Financial API | Nyitott | - |
|
||||||
|
| #137 | Implement Real-time System Health Monitor | Nyitott | - |
|
||||||
|
| #136 | Implement AI Researcher Logs Backend & Frontend | Nyitott | - |
|
||||||
|
| #135 | Connect User Management Table to Real API | Nyitott | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 LEZÁRT JEGYEK (140 db)
|
||||||
|
|
||||||
|
### V0 - Rendszertisztítás és Alapozás (Lezárt)
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #171 | Audit & Cleanup: Frontend mappák | Lezárt | V0 - Rendszertisztítás és Alapozás |
|
||||||
|
| #170 | Audit & Cleanup: Backend mappák | Lezárt | V0 - Rendszertisztítás és Alapozás |
|
||||||
|
| #169 | Audit & Cleanup: Gyökérkönyvtár (Root) | Lezárt | V0 - Rendszertisztítás és Alapozás |
|
||||||
|
|
||||||
|
### Phase 1: Core Functionality Fixes (Lezárt)
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #144 | Fix Authentication Endpoint Mismatch | Lezárt | Phase 1: Core Functionality Fixes |
|
||||||
|
| #143 | Repair Expense API 500 Errors | Lezárt | Phase 1: Core Functionality Fixes |
|
||||||
|
| #141 | Fix Vehicle Creation Endpoint Mismatch | Lezárt | Phase 1: Core Functionality Fixes |
|
||||||
|
|
||||||
|
### Milestone 14: Public API & Feature Parity
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #131 | API & Wiring: Analytics & TCO Dashboard | Lezárt | Milestone 14: Public API & Feature Parity |
|
||||||
|
| #130 | API & Wiring: Gamification Engine | Lezárt | Milestone 14: Public API & Feature Parity |
|
||||||
|
| #129 | API & Wiring: Dual-UI User Preferences | Lezárt | Milestone 14: Public API & Feature Parity |
|
||||||
|
| #128 | API & Wiring: Quick Actions (Expenses & Vehicle Add) | Lezárt | Milestone 14: Public API & Feature Parity |
|
||||||
|
| #127 | API & Wiring: Vehicle Management (CRUD) | Lezárt | Milestone 14: Public API & Feature Parity |
|
||||||
|
|
||||||
|
### Epic 11 (Public UI) Jegyek
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #126 | Service Finder teszt users | Lezárt | - |
|
||||||
|
| #125 | CRITICAL UI FIX: Login Page & Dashboard | Lezárt | - |
|
||||||
|
| #124 | Epic 11 - Ticket 6: Vehicle Analytics & TCO Dashboard | Lezárt | Epic 11 (Public UI) Jegyek Létrehozása |
|
||||||
|
| #123 | Epic 11 - Ticket 5: Trophy Showcase & Gamification Hub | Lezárt | Epic 11 (Public UI) Jegyek Létrehozása |
|
||||||
|
| #122 | Epic 11 - Ticket 4: Quick Action Buttons | Lezárt | Epic 11 (Public UI) Jegyek Létrehozása |
|
||||||
|
| #121 | Epic 11 - Ticket 3: Garage Tile System with Vehicle Cards | Lezárt | Epic 11 (Public UI) Jegyek Létrehozása |
|
||||||
|
| #120 | Epic 11 - Ticket 2: Daily Quiz System with Rewards | Lezárt | - |
|
||||||
|
| #119 | Epic 11 - Ticket 1: Dual-UI Profile Selector | Lezárt | Epic 11 (Public UI) Jegyek Létrehozása |
|
||||||
|
| #118 | Epic 11: The Smart Garage (Public Frontend) | Lezárt | Epic 11 (Public UI) Jegyek Létrehozása |
|
||||||
|
|
||||||
|
### Epic 10 (Admin UI) Jegyek
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #117 | Epic 10 - Ticket 5: AI Pipeline & Financial Dashboard | Lezárt | Epic 10 (Admin UI) Jegyek Létrehozása |
|
||||||
|
| #116 | Epic 10 - Ticket 4: User Management Integration | Lezárt | Epic 10 (Admin UI) Jegyek Létrehozása |
|
||||||
|
| #115 | Epic 10 - Ticket 3: Geographical Map View | Lezárt | Epic 10 (Admin UI) Jegyek Létrehozása |
|
||||||
|
| #114 | Epic 10 - Ticket 2: Launchpad UI & Module Switcher | Lezárt | Epic 10 (Admin UI) Jegyek Létrehozása |
|
||||||
|
| #113 | Epic 10 - Ticket 1: RBAC Implementation | Lezárt | Epic 10 (Admin UI) Jegyek Létrehozása |
|
||||||
|
| #112 | Epic 10: Mission Control (Admin Dashboard) | Lezárt | Epic 10 (Admin UI) Jegyek Létrehozása |
|
||||||
|
|
||||||
|
### Epic 9: UltimateSpecs Pipeline
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #111 | Epic 9: 5-Szintes AI-Vezérelt Szerviz Validáció | Lezárt | - |
|
||||||
|
| #110 | Epic 9: Tests & Scripts (60 fájl) | Lezárt | - |
|
||||||
|
| #109 | Epic 9: Models, Schemas & Core (55 fájl) | Lezárt | - |
|
||||||
|
| #108 | Epic 9: API Endpoints & Routers (26 fájl) | Lezárt | - |
|
||||||
|
| #107 | Epic 9: Services & Üzleti Logika Auditja | Lezárt | - |
|
||||||
|
| #106 | Epic 9: Workers & Robotok Auditja (49 fájl) | Lezárt | - |
|
||||||
|
| #105 | Admin javítások | Lezárt | - |
|
||||||
|
| #91 | Worker: vehicle_ultimate_r3_finalizer | Lezárt | - |
|
||||||
|
| #90 | Worker: vehicle_ultimate_r2_enricher | Lezárt | EPIC 9: UltimateSpecs Pipeline Overhaul |
|
||||||
|
| #89 | Worker: vehicle_ultimate_r1_scraper | Lezárt | EPIC 9: UltimateSpecs Pipeline Overhaul |
|
||||||
|
| #88 | Worker: vehicle_ultimate_r0_spider | Lezárt | EPIC 9: UltimateSpecs Pipeline Overhaul |
|
||||||
|
| #87 | DB: Extend ExternalReferenceLibrary with UltimateSpecs | Lezárt | EPIC 9: UltimateSpecs Pipeline Overhaul |
|
||||||
|
|
||||||
|
### v2.0 - Enterprise Admin Rendszer & Dinamikus Konfig
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #104 | Phase 4: Anti-Cheat és Biztonsági Audit | Lezárt | v2.0 - Enterprise Admin Rendszer & Dinamikus Konfig |
|
||||||
|
| #103 | Phase 3: Core Admin Végpontok és Monitor | Lezárt | v2.0 - Enterprise Admin Rendszer & Dinamikus Konfig |
|
||||||
|
| #102 | Phase 2: RBAC és Admin Jogosultságok Bővítése | Lezárt | v2.0 - Enterprise Admin Rendszer & Dinamikus Konfig |
|
||||||
|
| #101 | Phase 1: Hardcode Kivezetés és Dinamikus Konfiguráció | Lezárt | v2.0 - Enterprise Admin Rendszer & Dinamikus Konfig |
|
||||||
|
|
||||||
|
### Epic 8: Gamification 2.0
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #83 | Gamification 2.0: API végpontok frissítése | Lezárt | Epic 8 Gamification 2.0, Verseny és Önvéde |
|
||||||
|
| #82 | Gamification 2.0: SystemParameter konfiguráció | Lezárt | Epic 8 Gamification 2.0, Verseny és Önvéde |
|
||||||
|
| #81 | Gamification 2.0: Robot 5 (Auditor) implementáció | Lezárt | Epic 8 Gamification 2.0, Verseny és Önvéde |
|
||||||
|
| #80 | Gamification 2.0: Robot 3 (Enricher) refaktorálás | Lezárt | Epic 8 Gamification 2.0, Verseny és Önvéde |
|
||||||
|
| #79 | Gamification 2.0: Adatbázis migrációk implementálása | Lezárt | Epic 8 Gamification 2.0, Verseny és Önvéde |
|
||||||
|
|
||||||
|
### Epic 8: System Infrastructure & Admin Core
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #67 | Epic 8 - Admin 1: Hierarchikus System Parameter Engine | Lezárt | Epic 8: System Infrastructure & Admin Core |
|
||||||
|
|
||||||
|
### Epic 4.1: Bizalmi Motor (Social & Trust Engine)
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #66 | Epic 4.1 - Social 3: Verifikált Szerviz Vélemények | Lezárt | Epic 4.1 Bizalmi Motor |
|
||||||
|
| #65 | Epic 4.1 - Social 2: Gondos Gazda Index | Lezárt | Epic 4.1 Bizalmi Motor |
|
||||||
|
| #64 | Epic 4.1 - Social 1: Jármű Értékelési Rendszer | Lezárt | Epic 4.1 Bizalmi Motor |
|
||||||
|
|
||||||
|
### Epic 3: Economy & Billing Engine
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #63 | Economy 4: Financial Truth Verifikáció | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #62 | Economy 3: RBAC & Admin API | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #61 | Economy 2: FinancialOrchestrator & Unit Tests | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #60 | Economy 1: Adatmodell & Séma Bővítés | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #44 | Epic 3 Pénzügyi Motor - Szigorú Audit és Javítás | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #20 | Fejlesztés: Előfizetés életciklus kezelése | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #19 | Fejlesztés: Cron‑job ütemező beállítása | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #18 | Fejlesztés: Atomi tranzakciók bevezetése | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #17 | Fejlesztés: Billing Engine Service létrehozása | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #16 | Fejlesztés: Stripe Webhook implementálás | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
| #15 | Epic 3 Audit: Pénzügyi Motor és Főkönyv | Lezárt | 💰 Epic 3: Economy & Billing Engine |
|
||||||
|
|
||||||
|
### 8# DDD Database Refactoring 1.0
|
||||||
|
| ID | Cím | Státusz | Mérföldkő |
|
||||||
|
|----|-----|---------|-----------|
|
||||||
|
| #59 | DDD Refaktor 5.9/6: Seeding scriptek, külső adatforrások | Lezárt | 8# DDD Database Refactoring 1.0 |
|
||||||
|
| #58 | DDD Refaktor 5.5/6: Robotok és Workerek frissítése | Lezárt | 8# DDD Database Refactoring 1.0 |
|
||||||
|
| #57 | DDD Refaktor 2.9/6: Adatbázis Kényszerítések és Indexek | Lezárt | 8# DDD Database Refactoring 1.0 |
|
||||||
|
| #56 | DDD Refaktor 2.5/6: Teljes Metadata Szinkronizáció | Lezárt | 8# DDD Database Refactoring 1.0 |
|
||||||
|
| #54 | DDD Refaktor 1.95/6: A 'data' séma véglegesítése | Lezárt | 8# DDD Database Refactoring 1.0 |
|
||||||
|
| #53 | D
|
||||||
@@ -2613,9 +2613,9 @@ function publicAssetsURL(...path) {
|
|||||||
const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`;
|
const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`;
|
||||||
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`;
|
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`;
|
||||||
// @ts-expect-error file will be produced after app build
|
// @ts-expect-error file will be produced after app build
|
||||||
const getServerEntry = () => import('file:///app/.nuxt//dist/server/server.mjs').then((r) => r.default || r);
|
const getServerEntry = () => import('file:///app/.nuxt/dist/server/server.mjs').then((r) => r.default || r);
|
||||||
// @ts-expect-error file will be produced after app build
|
// @ts-expect-error file will be produced after app build
|
||||||
const getClientManifest = () => import('file:///app/.nuxt//dist/server/client.manifest.mjs').then((r) => r.default || r).then((r) => typeof r === "function" ? r() : r);
|
const getClientManifest = () => import('file:///app/.nuxt/dist/server/client.manifest.mjs').then((r) => r.default || r).then((r) => typeof r === "function" ? r() : r);
|
||||||
// -- SSR Renderer --
|
// -- SSR Renderer --
|
||||||
const getSSRRenderer = lazyCachedFunction(async () => {
|
const getSSRRenderer = lazyCachedFunction(async () => {
|
||||||
// Load server bundle
|
// Load server bundle
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"id":"dev","timestamp":1774557833950}
|
{"id":"dev","timestamp":1774810059036}
|
||||||
@@ -1 +1 @@
|
|||||||
{"id":"dev","timestamp":1774557833950,"prerendered":[]}
|
{"id":"dev","timestamp":1774810059036,"prerendered":[]}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"date": "2026-03-26T20:43:59.681Z",
|
"date": "2026-03-29T18:47:42.746Z",
|
||||||
"preset": "nitro-dev",
|
"preset": "nitro-dev",
|
||||||
"framework": {
|
"framework": {
|
||||||
"name": "nuxt",
|
"name": "nuxt",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"dev": {
|
"dev": {
|
||||||
"pid": 19,
|
"pid": 19,
|
||||||
"workerAddress": {
|
"workerAddress": {
|
||||||
"socketPath": "\u0000nitro-worker-19-1-1-9144.sock"
|
"socketPath": "\u0000nitro-worker-19-4-4-8465.sock"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
frontend/admin/.nuxt/nuxt.d.ts
vendored
6
frontend/admin/.nuxt/nuxt.d.ts
vendored
@@ -1,8 +1,8 @@
|
|||||||
/// <reference types="vuetify-nuxt-module" />
|
|
||||||
/// <reference types="@nuxtjs/i18n" />
|
|
||||||
/// <reference types="@pinia/nuxt" />
|
/// <reference types="@pinia/nuxt" />
|
||||||
/// <reference types="@nuxt/telemetry" />
|
|
||||||
/// <reference types="@nuxtjs/tailwindcss" />
|
/// <reference types="@nuxtjs/tailwindcss" />
|
||||||
|
/// <reference types="@nuxtjs/i18n" />
|
||||||
|
/// <reference types="vuetify-nuxt-module" />
|
||||||
|
/// <reference types="@nuxt/telemetry" />
|
||||||
/// <reference path="types/nitro-layouts.d.ts" />
|
/// <reference path="types/nitro-layouts.d.ts" />
|
||||||
/// <reference path="types/builder-env.d.ts" />
|
/// <reference path="types/builder-env.d.ts" />
|
||||||
/// <reference types="nuxt" />
|
/// <reference types="nuxt" />
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"_hash": "86WsHSzrghegd85QlSfb0tmyVB8WGKoWBHcdl2r1_DE",
|
|
||||||
"project": {
|
|
||||||
"rootDir": "/app"
|
|
||||||
},
|
|
||||||
"versions": {
|
|
||||||
"nuxt": "3.21.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 3/27/2026, 9:42:29 AM
|
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 3/29/2026, 6:47:41 PM
|
||||||
import "@nuxtjs/tailwindcss/config-ctx"
|
import "@nuxtjs/tailwindcss/config-ctx"
|
||||||
import configMerger from "@nuxtjs/tailwindcss/merger";
|
import configMerger from "@nuxtjs/tailwindcss/merger";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user