From e255fea3a52e371d9ba89d22c54dc218fea9ad50 Mon Sep 17 00:00:00 2001 From: Kincses Date: Tue, 10 Feb 2026 10:20:45 +0000 Subject: [PATCH] feat: implement pivot-currency model, rbac smart tokens & fix circular imports --- alembic.ini | 150 --- backend/.env | 13 + .../app/__pycache__/__init__.cpython-312.pyc | Bin .../app/api/__pycache__/deps.cpython-312.pyc | Bin 2564 -> 3502 bytes backend/app/api/deps.py | 72 +- .../__pycache__/assets.cpython-312.pyc | Bin 7525 -> 6522 bytes .../__pycache__/auth.cpython-312.pyc | Bin 6749 -> 6867 bytes .../__pycache__/billing.cpython-312.pyc | Bin 6221 -> 0 bytes .../__pycache__/expenses.cpython-312.pyc | Bin 2494 -> 0 bytes .../__pycache__/fleet.cpython-312.pyc | Bin 3050 -> 0 bytes .../__pycache__/reports.cpython-312.pyc | Bin 2771 -> 0 bytes .../__pycache__/users.cpython-312.pyc | Bin 836 -> 0 bytes .../__pycache__/vehicles.cpython-312.pyc | Bin 4076 -> 0 bytes backend/app/api/v1/endpoints/assets.py | 231 +++-- backend/app/api/v1/endpoints/auth.py | 73 +- .../core/__pycache__/__init__.cpython-312.pyc | Bin .../core/__pycache__/config.cpython-312.pyc | Bin 3410 -> 2633 bytes .../app/core/__pycache__/i18n.cpython-312.pyc | Bin 1925 -> 2438 bytes .../core/__pycache__/security.cpython-312.pyc | Bin 2768 -> 3218 bytes .../__pycache__/validators.cpython-312.pyc | Bin 3008 -> 0 bytes backend/app/core/config.py | 45 +- backend/app/core/i18n.py | 36 +- backend/app/core/rbac.py | 40 + backend/app/core/security.py | 27 +- .../db/__pycache__/__init__.cpython-312.pyc | Bin .../app/db/__pycache__/base.cpython-312.pyc | Bin 689 -> 1260 bytes .../db/__pycache__/session.cpython-312.pyc | Bin backend/app/db/base.py | 18 +- backend/app/diagnose_system.py | 91 ++ backend/app/locales/hu.json | 23 +- backend/app/models/__init__.py | 45 +- .../__pycache__/__init__.cpython-312.pyc | Bin 890 -> 1493 bytes .../models/__pycache__/asset.cpython-312.pyc | Bin 7192 -> 8701 bytes .../__pycache__/company.cpython-312.pyc | Bin 3321 -> 0 bytes .../__pycache__/core_logic.cpython-312.pyc | Bin 0 -> 2781 bytes .../__pycache__/document.cpython-312.pyc | Bin 1693 -> 1699 bytes .../__pycache__/history.cpython-312.pyc | Bin 0 -> 2134 bytes .../__pycache__/identity.cpython-312.pyc | Bin 5083 -> 5520 bytes .../__pycache__/organization.cpython-312.pyc | Bin 4462 -> 4509 bytes .../__pycache__/system_config.cpython-312.pyc | Bin 0 -> 1251 bytes .../__pycache__/vehicle.cpython-312.pyc | Bin 6059 -> 0 bytes backend/app/models/asset.py | 113 +-- backend/app/models/company.py | 63 -- backend/app/models/core_logic.py | 5 +- backend/app/models/document.py | 3 +- backend/app/models/email_log.py | 17 - backend/app/models/email_provider.py | 21 - backend/app/models/email_system.py | 30 - backend/app/models/email_template.py | 17 - backend/app/models/expense.py | 50 - backend/app/models/history.py | 53 +- backend/app/models/identity.py | 28 +- backend/app/models/organization.py | 26 +- backend/app/models/system.py | 13 + backend/app/models/system_config.py | 16 + backend/app/models/vehicle_catalog.py | 54 - .../schemas/__pycache__/asset.cpython-312.pyc | Bin 2419 -> 0 bytes .../__pycache__/asset_cost.cpython-312.pyc | Bin 0 -> 2586 bytes .../schemas/__pycache__/auth.cpython-312.pyc | Bin 2898 -> 3784 bytes .../__pycache__/organization.cpython-312.pyc | Bin 1991 -> 2218 bytes .../schemas/__pycache__/user.cpython-312.pyc | Bin 2999 -> 0 bytes backend/app/schemas/asset.py | 85 +- backend/app/schemas/asset_cost.py | 35 + backend/app/schemas/auth.py | 19 +- backend/app/schemas/auth_old.py | 46 - backend/app/schemas/organization.py | 2 + backend/app/scripts/seed_system_params.py | 43 + backend/app/seed_system.py | 127 ++- backend/app/seed_test_scenario.py | 107 ++ .../__pycache__/auth_service.cpython-312.pyc | Bin 13709 -> 14323 bytes .../__pycache__/cost_service.cpython-312.pyc | Bin 0 -> 5417 bytes .../__pycache__/email_manager.cpython-312.pyc | Bin 6387 -> 6077 bytes .../gamification_service.cpython-312.pyc | Bin 1721 -> 2391 bytes .../harvester_robot.cpython-312.pyc | Bin 4204 -> 0 bytes .../__pycache__/media_service.cpython-312.pyc | Bin 2674 -> 0 bytes backend/app/services/asset_service.py | 35 + backend/app/services/auth_service.py | 52 +- backend/app/services/cost_service.py | 97 ++ backend/app/services/email_manager.py | 14 +- backend/app/services/gamification_service.py | 55 +- backend/app/services/harvester_base.py | 43 +- backend/app/services/recon_bot.py | 51 + backend/app/services/robot_manager.py | 29 +- .../__pycache__/env.cpython-312.pyc | Bin 0 -> 3226 bytes backend/migrations/env.py | 39 +- .../versions/0adbe75a0b3f_complete_sync.py | 554 ++++++++++ ..._fix_identity_scope_and_finalize_asset_.py | 222 ++++ ...0adbe75a0b3f_complete_sync.cpython-312.pyc | Bin 0 -> 49482 bytes ...ular_imports_and_finalize_.cpython-312.pyc | Bin 0 -> 23019 bytes ..._scope_and_finalize_asset_.cpython-312.pyc | Bin 0 -> 22622 bytes .../c21c2c7e70d4_clean_gamification_setup.py | 152 --- docker-compose.yml | 12 +- docs/V01_gemini/11_Gamification_Social.md | 16 +- docs/V01_gemini/15_Changelog.md | 42 +- .../18_ASSET_AND_FLEET_SPECIFICATION.md | 17 +- .../19_ADMIN_AND_PERMISSIONS_SPEC.md | 18 +- docs/V01_gemini/_00_gemini_gem_kód | 132 +++ migrations/README | 1 - migrations/__pycache__/env.cpython-312.pyc | Bin 3643 -> 0 bytes migrations/env.py | 75 -- migrations/script.py.mako | 28 - ...ee8967_fix_roles_and_universal_vehicles.py | 38 - ...dd_verification_tokens_and_legal_tables.py | 607 ----------- .../13d050e8cf6d_initial_baseline_v2.py | 200 ---- .../553ef1388276_rebuild_schema_v2.py | 263 ----- ...6900f0b_add_persons_and_owner_person_id.py | 28 - .../8d450e9dc77f_add_vehicle_staging.py | 35 - ...les_and_universal_vehicles.cpython-312.pyc | Bin 2341 -> 0 bytes ...on_tokens_and_legal_tables.cpython-312.pyc | Bin 62158 -> 0 bytes ...e8cf6d_initial_baseline_v2.cpython-312.pyc | Bin 18123 -> 0 bytes ...f1388276_rebuild_schema_v2.cpython-312.pyc | Bin 19008 -> 0 bytes ...ersons_and_owner_person_id.cpython-312.pyc | Bin 1009 -> 0 bytes ...9dc77f_add_vehicle_staging.cpython-312.pyc | Bin 1382 -> 0 bytes ...4_clean_gamification_setup.cpython-312.pyc | Bin 13066 -> 0 bytes ...2ed020b1_merge_identity_v1.cpython-312.pyc | Bin 104988 -> 0 bytes .../c21c2c7e70d4_clean_gamification_setup.py | 152 --- .../fba92ed020b1_merge_identity_v1.py | 945 ------------------ 117 files changed, 2247 insertions(+), 3542 deletions(-) delete mode 100755 alembic.ini create mode 100644 backend/.env mode change 100755 => 100644 backend/app/__pycache__/__init__.cpython-312.pyc delete mode 100755 backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc delete mode 100755 backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc delete mode 100755 backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc delete mode 100755 backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc delete mode 100755 backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc delete mode 100755 backend/app/api/v1/endpoints/__pycache__/vehicles.cpython-312.pyc mode change 100755 => 100644 backend/app/core/__pycache__/__init__.cpython-312.pyc delete mode 100644 backend/app/core/__pycache__/validators.cpython-312.pyc create mode 100644 backend/app/core/rbac.py mode change 100755 => 100644 backend/app/db/__pycache__/__init__.cpython-312.pyc mode change 100755 => 100644 backend/app/db/__pycache__/session.cpython-312.pyc create mode 100644 backend/app/diagnose_system.py delete mode 100755 backend/app/models/__pycache__/company.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/core_logic.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/history.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/system_config.cpython-312.pyc delete mode 100644 backend/app/models/__pycache__/vehicle.cpython-312.pyc delete mode 100755 backend/app/models/company.py delete mode 100755 backend/app/models/email_log.py delete mode 100755 backend/app/models/email_provider.py delete mode 100755 backend/app/models/email_system.py delete mode 100755 backend/app/models/email_template.py delete mode 100755 backend/app/models/expense.py create mode 100644 backend/app/models/system.py create mode 100644 backend/app/models/system_config.py delete mode 100755 backend/app/models/vehicle_catalog.py delete mode 100644 backend/app/schemas/__pycache__/asset.cpython-312.pyc create mode 100644 backend/app/schemas/__pycache__/asset_cost.cpython-312.pyc delete mode 100755 backend/app/schemas/__pycache__/user.cpython-312.pyc create mode 100644 backend/app/schemas/asset_cost.py delete mode 100755 backend/app/schemas/auth_old.py create mode 100644 backend/app/scripts/seed_system_params.py create mode 100644 backend/app/seed_test_scenario.py create mode 100644 backend/app/services/__pycache__/cost_service.cpython-312.pyc delete mode 100644 backend/app/services/__pycache__/harvester_robot.cpython-312.pyc delete mode 100644 backend/app/services/__pycache__/media_service.cpython-312.pyc create mode 100644 backend/app/services/asset_service.py create mode 100644 backend/app/services/cost_service.py create mode 100644 backend/app/services/recon_bot.py create mode 100644 backend/migrations/__pycache__/env.cpython-312.pyc create mode 100644 backend/migrations/versions/0adbe75a0b3f_complete_sync.py create mode 100644 backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py create mode 100644 backend/migrations/versions/__pycache__/0adbe75a0b3f_complete_sync.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc create mode 100644 backend/migrations/versions/__pycache__/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.cpython-312.pyc delete mode 100755 backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py delete mode 100755 migrations/README delete mode 100755 migrations/__pycache__/env.cpython-312.pyc delete mode 100755 migrations/env.py delete mode 100755 migrations/script.py.mako delete mode 100755 migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py delete mode 100755 migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py delete mode 100755 migrations/versions/13d050e8cf6d_initial_baseline_v2.py delete mode 100755 migrations/versions/553ef1388276_rebuild_schema_v2.py delete mode 100755 migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py delete mode 100755 migrations/versions/8d450e9dc77f_add_vehicle_staging.py delete mode 100755 migrations/versions/__pycache__/10b73fee8967_fix_roles_and_universal_vehicles.cpython-312.pyc delete mode 100755 migrations/versions/__pycache__/13bd03551ebf_add_verification_tokens_and_legal_tables.cpython-312.pyc delete mode 100755 migrations/versions/__pycache__/13d050e8cf6d_initial_baseline_v2.cpython-312.pyc delete mode 100755 migrations/versions/__pycache__/553ef1388276_rebuild_schema_v2.cpython-312.pyc delete mode 100755 migrations/versions/__pycache__/5aed26900f0b_add_persons_and_owner_person_id.cpython-312.pyc delete mode 100755 migrations/versions/__pycache__/8d450e9dc77f_add_vehicle_staging.cpython-312.pyc delete mode 100755 migrations/versions/__pycache__/c21c2c7e70d4_clean_gamification_setup.cpython-312.pyc delete mode 100644 migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc delete mode 100755 migrations/versions/c21c2c7e70d4_clean_gamification_setup.py delete mode 100644 migrations/versions/fba92ed020b1_merge_identity_v1.py diff --git a/alembic.ini b/alembic.ini deleted file mode 100755 index f07ee97..0000000 --- a/alembic.ini +++ /dev/null @@ -1,150 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts. -# this is typically a path given in POSIX (e.g. forward slashes) -# format, relative to the token %(here)s which refers to the location of this -# ini file -script_location = %(here)s/migrations - - -# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s -# Uncomment the line below if you want the files to be prepended with date and time -# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file -# for all available tokens -# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s -# Or organize into date-based subdirectories (requires recursive_version_locations = true) -# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. for multiple paths, the path separator -# is defined by "path_separator" below. -prepend_sys_path = . - - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the tzdata library which can be installed by adding -# `alembic[tz]` to the pip requirements. -# string value is passed to ZoneInfo() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to /versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "path_separator" -# below. -# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions - -# path_separator; This indicates what character is used to split lists of file -# paths, including version_locations and prepend_sys_path within configparser -# files such as alembic.ini. -# The default rendered in new alembic.ini files is "os", which uses os.pathsep -# to provide os-dependent path splitting. -# -# Note that in order to support legacy alembic.ini files, this default does NOT -# take place if path_separator is not present in alembic.ini. If this -# option is omitted entirely, fallback logic is as follows: -# -# 1. Parsing of the version_locations option falls back to using the legacy -# "version_path_separator" key, which if absent then falls back to the legacy -# behavior of splitting on spaces and/or commas. -# 2. Parsing of the prepend_sys_path option falls back to the legacy -# behavior of splitting on spaces, commas, or colons. -# -# Valid values for path_separator are: -# -# path_separator = : -# path_separator = ; -# path_separator = space -# path_separator = newline -# -# Use os.pathsep. Default configuration used for new projects. -path_separator = os - -# set to 'true' to search source files recursively -# in each "version_locations" directory -# new in Alembic version 1.10 -# recursive_version_locations = false - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -# database URL. This is consumed by the user-maintained env.py script only. -# other means of configuring database URLs may be customized within the env.py -# file. -sqlalchemy.url = postgresql+asyncpg://user:pass@postgres-db:5432/service_finder - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module -# hooks = ruff -# ruff.type = module -# ruff.module = ruff -# ruff.options = check --fix REVISION_SCRIPT_FILENAME - -# Alternatively, use the exec runner to execute a binary found on your PATH -# hooks = ruff -# ruff.type = exec -# ruff.executable = ruff -# ruff.options = check --fix REVISION_SCRIPT_FILENAME - -# Logging configuration. This is also consumed by the user-maintained -# env.py script only. -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARNING -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARNING -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..1caad0d --- /dev/null +++ b/backend/.env @@ -0,0 +1,13 @@ + +# Database +DATABASE_URL=postgresql+asyncpg://service_finder_app:JELSZAVAD@db:5432/service_finder + +# Security +SECRET_KEY=ide_generálj_egy_hosszú_véletlen_karaktersort + +# Initial Admin (Ezt fogja a seed script használni) +INITIAL_ADMIN_EMAIL=kincses@valami.hu +INITIAL_ADMIN_PASSWORD=Kincs€s74 + +# Debug mód (opcionális) +DEBUG=True \ No newline at end of file diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index 4eb33d6833c5a221aa9fab5a805d735d176ae135..b57151682a0ceebb082c9f5555c6b5fe8f6c5192 100644 GIT binary patch literal 3502 zcmb7HU2GHC6}~fLkN;xFi5>C>$?PVi@pf?nghB(m)YPFs*g#3BT36la#B<|}Ju~*+ z83&w(HU-p%s#23y1*)YINc+%;plU^>K9q;9ES~z}Y>*PBAW~J8x^Ia>rFNfs?!=P> zv=8l-=Kh{@?z!iF=bSr#2?qTLO8ffHl$$<;{!Rx@@l-n--3&svkc?zTMH-W0G&aR* zT#94p9ILugE@(N`t??;d^Q1hQH|4e0T&hp=r~G!`tp>E3RE^!|)u0wih3vjZt<}P* zu-*5nby|I@UW=q6_SmOJwT4s!gII)J*|#0Vwp-kIA9SN7)dVzt*}o9jnrVg^9ydGV zF-ESzEpiaExCL`#Y%_3;x#ZAwcdFH<3_FzesMQ&bv2rc0k;6OrKuW_HFV_L@Zn+-s z0h-8lSE{YV#-g85bz+?5Jy$T5ye_GhE2*SSi%aOGA(;0haRKYHVFgc(jGQ01oW?et z#T%w%7L6EVMb9OQ=J5lt*K% zRU_kcUdE!C&tcv2UPufLiti=P1C8sVfyrcjk5o{4CieG8utkrW&nQ%q*CxM6RLi}| z$P;W9iJq=nmmUf6@Lhv%<1$o(hyhGY0VZ|OBPXE!IW0_QM#z+GZu{^A78`2{G0ieAD*5hTG@S|~^* zH808g@}`hHA&i$FOqA~#f?l3ibLB^BT(Fr2CIu54lZv3?acn-Gn<&qNfJ`Y*3?ZA( zDrF#gS9zE}TvcBx5Y8qpO~(#SCg@LHIlh;3!l-rANjEBVH(3%@kbeA|Y5eH*qCa z`N&@2tA)?_4pjd{|DHx`A><8K!X2M_I#wgQR-(d6v~i`iW2I@&dVml6*AVaXzYL?= z*4u}cLT^le<7uG&S*Z0{Q`>S=-(pkWO62HEH&+XEoX@}JMKzJ>q5l{ZCs&X3GZrh4 zk`5T8tGIwJLiC{5;M@2iV6KWwCcA|{O0rpGW($ez6#IW5kzL?+AQ5FDQodfyfHPy( zI6Y^#EH%+Y4I`Jtd=O)w=D_8XVX z^22y6XmNn=ZI>Z$0+;2%mvOpiVnVN#(EGF8m&P%{7OTh>r|70tZ=@wvB4S?0VxEXP zw3c5nL^vEav24{>U9V3ZIyWMoJa=&@8FLfr<2HmX4?U9z(jeB-pkuk{gy9APYCeH< z(p|$losSZbgE0Li_>5Vo9G|P*^^4q%-0i(fA;Iyf1%A0Zx!9eoyf^+Nq*T1hYNX*- z|6Ko_v5!VRRF~U&7u$OO5dCB0$BmD^{%6mh&MxjhwbXWcDRO2xcxL+KN}z5z(771s zTncneC;t|xo$b4I{FleUSE<~@Vt1l)Mtu^}Dqd|hP&+-i7DS;z=5vDr_iEy+C*EN` zIm(iXnZ0>Quv68u3?Ay^h^1bij0&=UiO(Z;u-`YB|U?c8B*`MHC5$l}a)-Y~cF zhDoDrr*k3@+K2A`N^dMC?0Z+Z zfHRQ56Et6Z*;Z&XrtOSa^jy@G8w9FfGc?nFaYU0@Co<7<-e7ZJG1C4&=UDCr`ys@T z-*O9XN6uBI#K$~CWH;0hZNTwy2Hq}@=Uz#b)WNmKA44+3<~W{=3*Ez$ zV%d?(d$Dr5jENL4G*)?0&fgLnDgF-y_6W_&Aw z=9ug^1$;B-gg87JhBb52@>xy*r}-Ecga~;r#@!< z9;!8tVVD=FYY}z*6@^})1B>XubF}|C>Uoa#`~$u50`)ydovXfYyXyAMbj%nt-9LVR!wuv01E{I} z_QbD#^t5enrR(@o+t=3Jbxe508(rg|T@BXFcFY>H-8bG}qw{M#;{6*Q+Up83O?Nw9 JB53Rb{TEO3gRcMp literal 2564 zcmcguOKcNI7@qZG{Yq@d2_z&mEFpykA&1Z)5VTZ*gz{2?ASsg7YPI%Gl6BT=cXl>l z*N6zJmP$PoRca!&Jw)Y#fCB=RLpgv}oO;1^rGzO_soF~AW(0?pL)CwrEGCpn?WH4m z=lkb>%*;Rk!{64|2N8^beOCKZMCfNuIK#ID9^C^ljZ~xxI?4zMLEzY>yAm##-MTyD zNq92egf}B5#469D`!fE7zlyzjAQMakt60=SnYu(>75nu1OgIs)V!s~AL=({}4(JV; zSRy7M7s9Uedgm~;88=-N7OBJvknyX*OQEMLD`CZh!wy}gRyx$EORd8}wH`OCVH_J3 z)yVtaL`&Y)6@9=Bb_vWsl%rbKQ1rnrH}m!595z&w)$bb~K0I(Dg{w>^npB~d3F1vd zeHIr&1m-fH z5fhU_tXs)x-IG1t3K;Izvtt_P`Kv-BL}xBF$-GsHi3C%N7%5lHHOLS64E!G5gw=yc zfM+WTsxa{iLgcL?N-sI?84X0;4n;iJ<(p7Y&L(i!Kd2wm!D5CWaw66}~_aE-vT9bB= z>R)zmZ+f-EsmaF7~UZtW=AhCUHKj&4IAzjN7rT<8i~UxI8#gLLX4e4gk30%OGN~2gGSImL|Z2KyWSIrIE zvzhFeLbB5phxQ9!@S4GF~Q8GsVr!w z6kQ>5*1&{kF!Ayav<}2p@G>d)Ztjss2Kz>a_YED{pBU(8ZYX)otr?WXYPxck$OhP8 zu_dKmd2nc0-a9lh*w2ERDXZWziq)t5SAlnA_x}F=fx#}3g!!ht5b!E!S~_LEoRZhG zipqQz)KM8u>fylTC*uJmNl%)fw&s?8TDOXGW;~Z?QC^W#79kKZGFKrRK!w{jH^NK) zhJK$yl^F8Z--)jK&bMy9dDV2`T;crsOHE&Gn(OE-b@W~vzJBb=v75q`x62*7%guY{ z8upwO=OZiTB2p$$3%A{CX`gH9Ew%K{H|~7wbw`2~ zM*Pi_!DLpX+n&n@jDRuQgq7zS4YS&rRPq zhe|#B%I*8hjR)rH4@~Wy4>in%I!mF>a%lZj|4*TabBLZ&OV51c)@6qMbswHOeQLSH z>&qnSqEiPeeFBR03x8BPQ23zm#N-iwy3aSzF5C(T1FJl@g1dWxuy+Xqt3CD`eQiM8 zUM;}4YZVh|KBpOD1iAuZK4^5%>Bg9ERDn#$X@s|;XF@Bwv>rDhG>Jy@gm)&k(tKA} z^wc=ceHX&%0(4(K##yK zBHT;N{~DOKbi9jf2a*>IGY6lFTM&c=)b;}k|A@Af(3S<%vw*r6&`ZCfwF_wD0_waU zXg%9J7icX7T4%PG1D#Xi-Prm&oBO|y4NOHIil}AH%;cx1?l!c{tSL8iJ@jrAqVxWy XiW}zpp>T!M6%mQS$38%08*KU)aCCj3 diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 36ced24..2550451 100755 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Dict, Any import logging from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer @@ -6,24 +6,27 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.db.session import get_db -from app.core.security import decode_token +from app.core.security import decode_token, RANK_MAP from app.models.identity import User logger = logging.getLogger(__name__) reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") -async def get_current_user( - db: AsyncSession = Depends(get_db), - token: str = Depends(reusable_oauth2), -) -> User: +async def get_current_token_payload( + token: str = Depends(reusable_oauth2) +) -> Dict[str, Any]: """ - Dependency, amely visszaadja az aktuálisan bejelentkezett felhasználót. - Támogatja a 'dev_bypass_active' tokent a fejlesztői teszteléshez. + Kinyeri a token payload-ot DB hívás nélkül. + Ez teszi lehetővé a gyors jogosultság-ellenőrzést. """ - # FEJLESZTŐI BYPASS if token == "dev_bypass_active": - result = await db.execute(select(User).where(User.id == 1)) - return result.scalar_one() + return { + "sub": "1", + "role": "superadmin", + "rank": 100, + "scope_level": "global", + "scope_id": "all" + } payload = decode_token(token) if not payload: @@ -31,27 +34,38 @@ async def get_current_user( status_code=status.HTTP_401_UNAUTHORIZED, detail="Érvénytelen vagy lejárt munkamenet." ) - - user_id: str = payload.get("sub") + return payload + +async def get_current_user( + db: AsyncSession = Depends(get_db), + payload: Dict[str, Any] = Depends(get_current_token_payload), +) -> User: + """ + Visszaadja a teljes User modellt. Akkor használjuk, ha módosítani kell az usert. + """ + user_id = payload.get("sub") if not user_id: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token azonosítási hiba." - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.") result = await db.execute(select(User).where(User.id == int(user_id))) user = result.scalar_one_or_none() - if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="A felhasználó nem található." - ) - - if user.is_deleted: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Ez a fiók korábban törlésre került." - ) + if not user or user.is_deleted: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="A felhasználó nem található.") - return user \ No newline at end of file + return user + +def check_min_rank(required_rank: int): + """ + Függőség-gyár: Ellenőrzi, hogy a felhasználó rangja eléri-e a minimumot. + Használat: Depends(check_min_rank(60)) -> RegionAdmin+ + """ + def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)): + user_rank = payload.get("rank", 0) + if user_rank < required_rank: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Nincs elegendő jogosultsága a művelethez. (Szükséges szint: {required_rank})" + ) + return True + return rank_checker \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc index b1a5b8dc08a47840b00f35e8ae1c9e7f10ee071f..0f8d0775f9480b8cb675a9eef1d140afb99d2a3d 100644 GIT binary patch literal 6522 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 literal 7525 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 diff --git a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc index 2324bfa01be15a22c44163f86db8b8dcf30da3f0..03380350bb4b50478f0601979bdcf7ddff2d6650 100644 GIT binary patch delta 3622 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 D^>w5; delta 3339 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 diff --git a/backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc deleted file mode 100755 index 89855718f7d15c6c777ff53d77cf19368b81e726..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6221 zcmd5=TWk|o8b0G0-(%#_y14$D# zrB%04(E{3P$*NQfZ$z|9TB^1WtE%8utJS_Za;v1XR8Xz7+P;~lQZG;YpEEPI69TK1 z_F+ewGw1gI=Q98Qf8RNOb2w}W9@~d6h2&y{{(%enLt3D)@+pPTCB!41il8VpNJR~U zhA2HqN0~u}!u5uTF=`q#kvttSM=gUEl4l~;sBO>|wGY}!*%&E^ItCq4=b)36O_9RE zLIV=0>)M!uMM6>daeV^0FK@mMbHESuHVC#=CA(f4Ht-g~1gZ5kV^(9yLo}||vAXo2 zMth#dQqbsDXOo}u1^PVx!yFxgi_DYZoq1ZzAELF8FM7B5xvhmxpyzX6eeSxJ-RRtb_vq7<+md(zdf8ZbPJ|?BlH>R=$8zkj5E%Smh?9v$Oi6MkIIb~)77^9;*# zGS`UzvOhp3CV{qWkE1ki}u0@n%X;S^1^bi>-6QmC!=%$D7{Yl-k;!Er*R?e0F zvE~mo>C&ysWwp?!zyy7ari=?YJE&izBACV5C)%5+8&tc8S!hR)1?L_CSC5Mms~p<| zU0;QFP8N7SCue)+yi;{dzbtYw zi3{LhE@gXZj)wi0_~HXz*3;MN?PCuFw_z8pJ|atIusB&LWmUL3BZ)c?uVa zC=3rFU$OY3VC-PhVkI=^oIu=wdlMVitVVnsYU*((0g`+hH)0z%5*sggq>b0_yy2Pm zd|AIYZEstw-+N`(xy}ovmufH8epRqBRlj%9-j-t8GBmMa#oqR_v>8SMJ8r;syp>rf zhukewZToiS)?*CLZ#P0ZS>(}vi{ybQA_!513B-9JK!ddYU>qT4{sExcBg~uzl**UW z}f?-z# zD$z>5?Y>T3AJiSB}FntDp5f+uiLZ6BW0>X81xE!15Bi zAe)>Ofl|dLWQf=hb0b1Hc~z8Q0kB0;$mUZvnQU4kP+z+wb8sk{HZ0oo>WxTgQuR3~shq}9f z=IQcowzG$Qwzso=zzcgnplgHOd4Rv^RFK^PTFEqF5mZ8<#wSM}KI?TbCOdcoyBe-? zQ(e6TSca&!C9p}KP*IS@C<&H0&b6{`TC9T>(G7`0iIeL5@S?S~k<~nCBY4T0fx(go zL~L-_+WbQk*l1t_NeN7DpJo=>m8#~YV&k~RP~8SH4TXjZ#VjQP0XPhiIUgegGv_4Z z+-6{#Ce;KKJIC`dD6x3O90ef`n7S>8o{CYD#n6~yOpJ{QqF9dCiqSwtRii3s0`3|O z$s({8U&BTui z%}y2S{F(}J5A?vVbOM}0Mdt!kS^>1M6403`way&N*idolCEG>Y+}>;D3uPZyzh8Z` zcB!djv8f~7tPb_o zKyC)0t9Zku!i$C1%r|WFwnf{n6)R(J!BQ<)swGo^rQEabH+L*Ct`y^1F5mQ_aUu9| z^!;dhXJ_j8@S;7KVuG0t3fYfPKW8eD@i_HU#t9>mwB;vIVJ!be8if5ld%#;k-J&RO z0dvdHUJZrYwUoDzxxE25Bq3kSBsc7V{O9GAx0Lz3q5|g|DX)w9+_Mq#UldX}EhcFx zNnJRdEXnbhpa5qXu!sRT#TSQSfk=WE{Gpg%0vn2Q-(kd64^L0O{4`t8H{zFfEhkuJgJu!KK=u!g&3z)SAy0U?{4uf45 z+Eoj|Sf9h+JRHO7vOo~PSO)Jv6x>t*QLx&PF(FsgT;f{mqVrcPX1egH z9}#VR3fdvDAZz84W#gh{hU$nGE<9s3_>=&Pd zIvjwcEs$ntilXkKwlr${26?_gHFr_dU9{ov$oZ|c+UnG!e*UYOzSoD>W&{ zrZmNVy?g&B<2PSkJo!S(A5WhgyN`BIBh;LfA>V5S-;*yE-<&bvrp)tHR>9_lfti{# z)q1bEWVy6*xqS226&vTqfA{izYZ-NnTDChgG|t?!7iEk%11l^p%a}4%l? My>Gy^B0f$08ySw(6aWAK diff --git a/backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc deleted file mode 100755 index 89a9077e7f4e4bb479ae68d455d3bde4e862c78a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2494 zcmZ`4TWlLeaPNJ#&yJHOAx&s=G^F4rBr0k^Dt(d~`b*ECgMah(SVlhn4@L10GI8!lgK0jT(KLRWG2ZW9ue+(?Z`|ckDf+YJ&$mN zXxD|1ooOPClMmHI)-5=CoeN2@!IQ{6)Y{${k7J|>YeX6oas0frc2;vKk!tvz8Au5< za`2fWC-X(0xKuer3M7X;8b3Tb`b_#vmK1y|pG$F6JLr{i*%9J-AuamkjGyAEJWhNA zzetm8%@$pkUe>k3=ZR9) zFmk3t41-1u!^z_!STm?*7^jP-UE2{n-=z(PVdipq-(>IVfs8a}=gsgGw+W7NH3l{T zxQ12%qQAQP%|f9zt^TQ<{Scu--pcu2KMC*Q^%Y8Pg6T299QGas@FuDxH_Z-L5{1sOG%1GEdT4Ri`H-GP zSa=0=-{g|Ev52Kl<+`PF+&bTsHQ%KF@lE<~&wv|L zj`|tl3S1Vk4Fk9=6dML`lN(q-0+$7T!@%l?YyNM12@ZkhEgKYotiQy+<#7Z@aqL5W z1U-%}egd-#X8beZWiF)^2S7Ed!zh0+slQadVf$YB+PLnPZ;VfuKl97iJff5Fl0I4f z*mb`Ba-w`=x-XEQeq z!d^oTUxO+RNSfm$6?0i4u=FHZR=FD(rH&ClJCV;3R|3*yU(Ib~fK`OBxUjlHVrQ|WY(KnWCF;!(|lO6mGg$$;gt4jDX#?%vPzVo)XOkM8aS4m-j7 z9>8zt?hLxqjN*-pjR%&sgEQ$$vS~hgCAu*1(PLL1U+LMq)U$VS+rH)G{*~zdnZZgd zacR%oo{Rf_NOW8k7IzKZObjoo!*^uV+*U>6W_1;b&5@boRSm`Bmv+qUxcq!2dEZKM z+fs5{rJ;4Dp>wIB6Oc=<&AkR2^V$_HV0SMybO(Z6OUbU=apggI6)7?0PozjnH2_V_ zU}`RPp=U*GSrl6;t(}X#gG;ePi{hbb4~iY&?p9kt`fjxeNv(f*yWn=`9vQr!` zj{6A>ETMs0sQ(t~_zCU2g|^;8n*skN(sZF?M!qA;-1f^y?jTsINyMvfcf8T@&a=y0 z^KBWmbuLW3`}+5r+7~-|mN)g@Ryw&fSBbY*1%~{rHvb_pWK}?X%Wa;afm442g8OPR diff --git a/backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc deleted file mode 100755 index 77c484504aaf57b778223279d178b3b6d8ac93f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3050 zcmbVOU2GHC6~6Q1pZF&~cH#|j364oN>jJS-NU0J?2_|$QB#R)sE13s(GIK-59gn?t z#z3-;WZ0^(qS7kTwqd1}HY@c36jlwY)O~1G?aHdus(r9EE7?0LqNp$JTY!B)JoVhM zCoxNCt6t5$_ndpqJ#&A~`Of`|-|sT^V=MZSlO)lJO)x8E?{S@eak8@hANj?NnMbZOJwU zu?REkjbA0(aeI2Ny@2gra;?J}XaQ{&d!FOGHaE{oZtQ@z<)@A(}h6&7dy@eNKQNcj^6WL5o zP#33hW)733miVQ*NHbY!PGp<5=m7dXs81fl(M=%+u263rB0~C>W}0uB(;)2wO}k!{ zHO&Nv`F1HVY0IIWAOFc~2-!B5*#b)YpO;gvz}gz`fm^-HA@oajmHU~)ftI-fmu`D; zhJD59j+e`p`2w%87t#6Vy$5W4`;R;Bb4x4TY3pzI=~uRCn=0_OJ-c6aykxHqTjPbj zIt$MKcdzHsVf6D`Q2RgQE;BJ^Cgvmb0*MbA(<9a$CnZa!zzC%i>B9R3D&k58Dhjy zS8#$Lujm?GYouPDMdLy)*Qj#*(r_GtDwma2U5n2v80$m1MYD}YN@g)-M*!Ia2B`OG ze}Lwn=xGss<3wHIkB?Tv2P@%&M(3ew=V+yK^!G=rV`nR4XUnrcuFi@T_{Kz|Qz}l^ zx_dtHeB@a>YIF}*1H;9sT3~lIFi;5$7=giRU|%J$&j=hSPCfL7YHb}iTEM(N_~GH3 zZ&ZDK6<^=42CLB{mFSUb^jIZ&?2E7woi=Mo6iarbLOeJ$zfM)tuCwqgAxa`%6k-(t(} zde$j|LRogg3GIMCg{`$P2sHi!3Vr*WAOU*yVBI<(pL3^Ew$~x~K4z`R-hjyNwIbU; zvvlG74KiuQesxZrc=VYASEMp%aSikbf4X!^$iQei0gQ`K z6$*5u8yd4EOD?AB(B+~ez!#zJVR}`3c?*r2(2Vq2Qm|}G$q0<>f~x%qnmSauQAby~ zix}-%@l>rdw0e5w^sU9)BOCiakKc=zzkA&1OjKJF#UIr=x>tKwdX0|0;^~LpuD`X0 zZ*%2C>Q}AVvO5ci6zr*m2WlN*s??Y();&^^;dm-WdCef2HQ}eGq#;c5`2q?<(_Mwa}is+Q!)D zjO^K`QpIYQ60C~W51fkZ#^YbFulH;xB^EOAUC%9r@vl#Vjc z&cs5dYhD1~$jO8v51~&G>mtmM_KqSb;sVYr4#B%xqqCsp*4E zF$AWDB%IR-#TG)7fEl3UmWOFW4Tx-HvnD|!RO%_xNvua48KO35GBNpFR)Z&+B#XLr zWp~gM7)JFAbsquic?FM?2^gbKD{TduO^#uh2WX^%M((5d1GM)68orNSy^r=kYzf>9 z7oC4+T-ST9_S~2#g5jh5{`Zo}n B!5aVo diff --git a/backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc deleted file mode 100755 index 99b55473220ad0c1876201d06dfe922ab23ede3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2771 zcmcguU2GIp6uvV%vwz(R{ab!oWT5M|uet2B_(2mfrOer+btv34y4C6>eDyO3`*UN?ZUY=t*uiL^x zuVBeGT@2fM?UpR)jVBlj0Slb7&(WmyC3)!f|3BeJgFHSK@)&yug9F|EUaQp}!(i9!{ zY3hjSo>NR-?N@9i$#H^w5}w&g$kDrqgS#;D*+Ot9h3t&jQe3FiFCQXhQ~4CqxG6q2 zaT2Mv<6w85AGduhifBYg2_Z3WTrw20J0hk;QfW8}OR8`RrNlN=STDRk!l~kjJ%tQc z!7^nptTFr34uu~?3V#Uhf5c&w6|Ed0PeGm=3!Z2Bu_Xd{jW|^MSGFGX78>hSp!(R% zQFO(2l=C^_-Qeezpry}Kn#44H#lVIkg|H5JPft<_4<@8g`VtBMcx@*EQsFOs~ z9#*4ABR&UV1ZX<&F{}XYP3HhMlp$HyO&jdM6bJOEY?y+oDTe9n3&V-D$e_<@#UzYX zO;0KEP4RFXlZ0s}n8tN*WceUfU`HUoh%%HzRauQxF!!pL9GWbL6=q65ht4|YQ#UHF z$RcN@a~3(vU8C*UYEtBwXGC(#_>{t-SMQvmN9g zcoF(!b-(~NROvkao5y!7?H1qN0^$GXJ76lYgsH?i=1YkomiiakR31P@rV=fddNa>b zVxFahcsn>+$a_x%mXc*PBujEqiW^a#rmstl(op((M2_W%2v`eihBPD}(WL$;u-GNq zFGupk^t1@=ml4zcuD%_+TDsQ(YvA?L+Jn$u@90=-5lt9E4bA7F7YNI;&=Y+YD2ng- zPie?{MKN?&cUyZmV<&4`YHRP=QC!=*yJPo$snOC2`$}~BL<^Rr1{C6D-4fO@(0DjC3v=e4Smup(Xw5B&hTQhd1>zFhx+uZ9iZ1||cy8=5m^ z!I_5Ui<`#U&Qy)povpj$d3Cy>d8RBlEd;YR1{$+0_>4;Mk`whUD*-YswZdgDNZ-}9 zEE8@n7C^kYOoYDEx4?7^$kdQy8e!je!dT4o(!;u}D?>P(@WW4sUuJ(IT9mLax#^x; z%(0jU=*bTO{3?!7!oKB%l`^nV?w`g6sQ_Lxh($0~7ADawSfCsiIfLP-8rShQ@*2#s zil<*dm$h*m_W-TAj{^5m?E|#&k*o5v+EM#M&hg38kCsk*>NDJ`d#$@~tw{%F`nBm$ zGSh$jFXne$zl;p*?T?7HPmFQ1WzrnN*yPCiBp ZxUCaaSvFi<|AY<9chzGaB+>v|`U~N!r@sII diff --git a/backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc deleted file mode 100755 index 7e8c21aa250724ed98d1d2a33759f7771ea4e682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 836 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 h=vY=9e~1E1h?$r3X1(NjWY%;6?xd diff --git a/backend/app/api/v1/endpoints/assets.py b/backend/app/api/v1/endpoints/assets.py index 235433f..3a98065 100644 --- a/backend/app/api/v1/endpoints/assets.py +++ b/backend/app/api/v1/endpoints/assets.py @@ -1,136 +1,129 @@ +import uuid +from typing import Any, Dict, List from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, func, and_ -import os -import logging +from sqlalchemy import select +from sqlalchemy.orm import selectinload from app.db.session import get_db from app.api.deps import get_current_user -from app.schemas.asset import AssetCreate, AssetResponse -from app.models.asset import Asset, AssetCatalog, AssetAssignment, AssetEvent +from app.models.asset import Asset, AssetCost, AssetTelemetry from app.models.identity import User -from app.models.organization import Organization, OrganizationMember, OrgType -from app.core.config import settings - -# VIN Validator - Standard 17 karakter, tiltott karakterek (I, O, Q) szűrése -class VINValidator: - @staticmethod - def validate(vin: str) -> bool: - vin = vin.upper() - if len(vin) != 17: - return False - if any(c in vin for c in "IOQ"): - return False - return True +from app.services.cost_service import cost_service +from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse router = APIRouter() -logger = logging.getLogger(__name__) -@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED) -async def create_asset( - asset_in: AssetCreate, - target_org_id: int = None, +# --- 1. MODUL: IDENTITÁS (Alapadatok) --- +@router.get("/{asset_id}", response_model=Dict[str, Any]) +async def get_asset_identity( + asset_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): - # 1. VIN Validáció - if not VINValidator.validate(asset_in.vin): - raise HTTPException(status_code=400, detail="Érvénytelen alvázszám (VIN) formátum!") - - # 2. Célflotta ellenőrzése - if not target_org_id: - stmt_org = select(Organization).join(OrganizationMember).where( - and_( - OrganizationMember.user_id == current_user.id, - Organization.org_type == OrgType.individual - ) - ) - org = (await db.execute(stmt_org)).scalar_one_or_none() - if not org: - raise HTTPException(status_code=404, detail="Privát flotta nem található. KYC szükséges.") - final_org_id = org.id - else: - # Céges jogosultság ellenőrzése - stmt_mem = select(OrganizationMember).where( - and_( - OrganizationMember.organization_id == target_org_id, - OrganizationMember.user_id == current_user.id - ) - ) - member = (await db.execute(stmt_mem)).scalar_one_or_none() - if not member or (member.role != "owner" and not (member.permissions or {}).get("can_add_asset")): - raise HTTPException(status_code=403, detail="Nincs jogod ehhez a flottához!") - final_org_id = target_org_id - - # 3. Katalógus ellenőrzése - stmt_cat = select(AssetCatalog).where( - and_( - AssetCatalog.make.ilike(asset_in.make), # Simán ilike, nem kell func() köré - AssetCatalog.model.ilike(asset_in.model) - ) - ) - catalog_item = (await db.execute(stmt_cat)).scalar_one_or_none() + """Csak a jármű alapadatai és katalógus információi.""" + stmt = select(Asset).where(Asset.id == asset_id).options(selectinload(Asset.catalog)) + asset = (await db.execute(stmt)).scalar_one_or_none() - if not catalog_item: - catalog_item = AssetCatalog( - make=asset_in.make, - model=asset_in.model, - vehicle_class=asset_in.vehicle_class, - fuel_type=asset_in.fuel_type - ) - db.add(catalog_item) - await db.flush() + if not asset: + raise HTTPException(status_code=404, detail="Jármű nem található") + + return { + "id": asset.id, + "vin": asset.vin, + "license_plate": asset.license_plate, + "name": asset.name, + "catalog": { + "make": asset.catalog.make, + "model": asset.catalog.model, + "type": asset.catalog.vehicle_class, + "factory_data": getattr(asset.catalog, 'factory_data', {}) + } + } - # 4. Asset létrehozása vagy betöltése (Shadow Identity) - stmt_exist = select(Asset).where(Asset.vin == asset_in.vin.upper()) - new_asset = (await db.execute(stmt_exist)).scalar_one_or_none() - - if not new_asset: - new_asset = Asset( - vin=asset_in.vin.upper(), - license_plate=asset_in.license_plate, - name=asset_in.name or f"{asset_in.make} {asset_in.model}", - year_of_manufacture=asset_in.year_of_manufacture, - fuel_type=asset_in.fuel_type, # JAVÍTVA: Most már átadjuk - vehicle_class=asset_in.vehicle_class, # JAVÍTVA: Most már átadjuk - mileage_unit=asset_in.reading_unit, # JAVÍTVA: Most már átadjuk - catalog_id=catalog_item.id, - quality_index=1.00, - system_mileage=0 - ) - db.add(new_asset) - await db.flush() - - # 5. Assignment - new_assignment = AssetAssignment( - asset_id=new_asset.id, - organization_id=final_org_id, - status="active" - ) - db.add(new_assignment) - - # 6. Kezdő KM esemény - if asset_in.current_reading: - db.add(AssetEvent( - asset_id=new_asset.id, - event_type="initial_reading", - recorded_mileage=asset_in.current_reading, - description="Kezdeti óraállás rögzítése", - data={"source": "user_registration"} - )) +# --- 2. MODUL: PÉNZÜGY (Költségek) --- +@router.get("/{asset_id}/costs", response_model=Dict[str, Any]) +async def get_asset_costs( + asset_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Pénzügyi modul: Helyi és EUR alapú összesítő, tételes lista.""" + stmt = select(AssetCost).where(AssetCost.asset_id == asset_id) + costs = (await db.execute(stmt)).scalars().all() + + summary_local = {} + summary_eur = {} + history = [] + + for c in costs: + cat = c.cost_type or "OTHER" + amt_local = float(c.amount_local) + amt_eur = float(c.amount_eur) if c.amount_eur else 0.0 + + summary_local[cat] = summary_local.get(cat, 0) + amt_local + summary_eur[cat] = summary_eur.get(cat, 0) + amt_eur + + history.append({ + "id": c.id, + "category": cat, + "amount_local": amt_local, + "currency_local": c.currency_local, + "amount_eur": amt_eur, + "exchange_rate": float(c.exchange_rate_used) if c.exchange_rate_used else 1.0, + "date": c.date + }) + + return { + "total_gross_local": sum(summary_local.values()), + "total_gross_eur": sum(summary_eur.values()), + "summary_local": summary_local, + "summary_eur": summary_eur, + "history": history + } +@router.post("/{asset_id}/costs", response_model=AssetCostResponse, status_code=status.HTTP_201_CREATED) +async def create_asset_cost( + asset_id: uuid.UUID, + cost_in: AssetCostCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + Új költség rögzítése. + Automatikus: EUR konverzió, Telemetria frissítés, XP jóváírás. + """ + # Validáció: az asset_id-nak egyeznie kell a path-szal + if cost_in.asset_id != asset_id: + raise HTTPException(status_code=400, detail="Asset ID mismatch") + try: - await db.commit() - await db.refresh(new_asset) - - # 7. NAS mappa struktúra - nas_base = getattr(settings, "NAS_STORAGE_PATH", "/opt/docker/dev/service_finder/nas/assets") - asset_path = os.path.join(nas_base, str(new_asset.id)) - os.makedirs(os.path.join(asset_path, "docs"), exist_ok=True) - os.makedirs(os.path.join(asset_path, "photos"), exist_ok=True) - - return new_asset + new_cost = await cost_service.record_cost( + db=db, + cost_in=cost_in, + user_id=current_user.id + ) + return new_cost except Exception as e: - await db.rollback() - logger.error(f"Asset Creation Error: {str(e)}") - raise HTTPException(status_code=500, detail="Hiba a mentés során.") \ No newline at end of file + raise HTTPException(status_code=500, detail=str(e)) + +# --- 3. MODUL: TELEMETRIA (Állapot) --- +@router.get("/{asset_id}/telemetry", response_model=Dict[str, Any]) +async def get_asset_telemetry( + asset_id: uuid.UUID, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """Műszaki állapot: KM óra, VQI (Quality) és DBS (Driving) pontszámok.""" + stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id) + tel = (await db.execute(stmt)).scalar_one_or_none() + + if not tel: + return {"current_mileage": 0, "vqi_score": 100.0, "dbs_score": 100.0} + + return { + "current_mileage": tel.current_mileage, + "vqi_score": float(tel.vqi_score), + "dbs_score": float(tel.dbs_score), + "last_update": tel.updated_at if hasattr(tel, 'updated_at') else None + } \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 279322b..9109092 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -5,7 +5,7 @@ from sqlalchemy import select from app.db.session import get_db from app.services.auth_service import AuthService -from app.core.security import create_access_token +from app.core.security import create_access_token, RANK_MAP from app.schemas.auth import ( UserLiteRegister, Token, PasswordResetRequest, UserKYCComplete, PasswordResetConfirm @@ -17,7 +17,7 @@ router = APIRouter() @router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED) async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)): - """Step 1: Alapszintű regisztráció (Email + Jelszó).""" + """Step 1: Alapszintű regisztráció. Az új felhasználó alapértelmezetten 'user' (Rank 10).""" stmt = select(User).where(User.email == user_in.email) result = await db.execute(stmt) if result.scalar_one_or_none(): @@ -28,7 +28,17 @@ async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(ge try: user = await AuthService.register_lite(db, user_in) - token = create_access_token(data={"sub": str(user.id)}) + + # Kezdeti token generálása + token_data = { + "sub": str(user.id), + "role": "user", + "rank": 10, + "scope_level": "individual", + "scope_id": str(user.id) + } + + token = create_access_token(data=token_data) return { "access_token": token, "token_type": "bearer", @@ -45,7 +55,7 @@ async def login( db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends() ): - """Bejelentkezés és Access Token generálása.""" + """Bejelentkezés és okos JWT generálása RBAC adatokkal.""" user = await AuthService.authenticate(db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -53,7 +63,20 @@ async def login( detail="Hibás e-mail cím vagy jelszó." ) - token = create_access_token(data={"sub": str(user.id)}) + # Szerepkör string kinyerése és rang meghatározása a RANK_MAP-ből + role_name = user.role.value if hasattr(user.role, 'value') else str(user.role) + user_rank = RANK_MAP.get(role_name, 10) + + token_data = { + "sub": str(user.id), + "role": role_name, + "rank": user_rank, + "scope_level": user.scope_level or "individual", + "scope_id": user.scope_id or str(user.id), + "region": user.region_code + } + + token = create_access_token(data=token_data) return { "access_token": token, "token_type": "bearer", @@ -62,14 +85,11 @@ async def login( @router.get("/verify-email") async def verify_email(token: str, db: AsyncSession = Depends(get_db)): - """E-mail megerősítése a kiküldött link alapján.""" + """E-mail megerősítése.""" success = await AuthService.verify_email(db, token) if not success: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Érvénytelen vagy lejárt megerősítő token." - ) - return {"message": "Email sikeresen megerősítve! Jöhet a profil kitöltése (KYC)."} + raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.") + return {"message": "Email sikeresen megerősítve!"} @router.post("/complete-kyc") async def complete_kyc( @@ -77,38 +97,27 @@ async def complete_kyc( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): - """Step 2: Személyes adatok és okmányok rögzítése.""" + """Step 2: KYC adatok rögzítése és aktiválás.""" user = await AuthService.complete_kyc(db, current_user.id, kyc_in) if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Felhasználó nem található.") - return {"status": "success", "message": "A profil adatok rögzítve, a rendszer aktiválva."} + raise HTTPException(status_code=404, detail="Felhasználó nem található.") + return {"status": "success", "message": "A profil aktiválva."} @router.post("/forgot-password") async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)): - """Elfelejtett jelszó folyamat indítása biztonsági korlátokkal.""" + """Elfelejtett jelszó folyamat.""" result = await AuthService.initiate_password_reset(db, req.email) - if result == "cooldown": - raise HTTPException(status_code=429, detail="Kérjük várjon 2 percet az újabb kérés előtt.") - if result in ["hourly_limit", "daily_limit"]: - raise HTTPException(status_code=429, detail="Túllépte a napi/óránkénti próbálkozások számát.") - - return {"message": "Amennyiben a megadott e-mail cím szerepel a rendszerünkben, kiküldtük a linket."} + raise HTTPException(status_code=429, detail="Kérjük várjon 2 percet.") + return {"message": "Amennyiben a cím létezik, a linket kiküldtük."} @router.post("/reset-password") async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)): - """Új jelszó beállítása. Backend ellenőrzi az egyezőséget és a tokent.""" + """Új jelszó beállítása.""" if req.password != req.password_confirm: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="A két jelszó nem egyezik meg." - ) + raise HTTPException(status_code=400, detail="A jelszavak nem egyeznek.") success = await AuthService.reset_password(db, req.email, req.token, req.password) if not success: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Érvénytelen adatok vagy lejárt token." - ) - - return {"message": "A jelszó sikeresen frissítve! Most már bejelentkezhet."} \ No newline at end of file + raise HTTPException(status_code=400, detail="Hiba a jelszó frissítésekor.") + return {"message": "A jelszó sikeresen frissítve!"} \ No newline at end of file diff --git a/backend/app/core/__pycache__/__init__.cpython-312.pyc b/backend/app/core/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 2754663252494d94315d74c1901a76185921f085..277aaa3a532e8d76be18886e6c684d1552809312 100644 GIT binary patch delta 1491 zcmY+DO>7%Q6vt=n+4Zi!8pofpoy3mwQM)8*ArVnjO5AuGOcOh@9Z(0PmFwL!i~JGR z4%AVQ9jS)`mqc5M9JxXe9Jo{n?jSA!U#vw{d!)BSjRZ$##s*SL{?GgU=k0srH{Rca zzj&O#IviGjKg`r;>LbgcGtkOx$FclhvldXScbA zTB{bzhdGUS9Dl%~VLZ|iIU!$> zUy-N3nK_2|H28x?o8kCXeFEMYzh$r~@#e;g;ml+UQeG@bsbpqF%o*0axSYdyTfC!x z3+K?TuP9g4>QY_NK2l4{rdpL1ZDwo7aHlirLOQuBB~xqZj3lll)2q8ek|bYy^|h$s zdwG97na{tQ&7}-`Dp^S0Oy))XFPIR8oR~`IrH$OG0gF40n&G5{r1f0(opegf?GC8b z&DzqoR@+qX)*7U-i)Q4dTe<97udV(b8)h>4$Lw%Iy=?-(5b|q zh(pbyJNbkyhCv%AGNCW?%D@#I)@xY)1b?@|sp$fmOMqrYps7+ zE|yDMN~NihF&e)AjTz;BW2RVdR!eHFcQ<6>2LE4~H%XZ#BIG^{{yM&@ow9O6TfxZS vZ#{wK)Qta7i9dkw6hxka8&5#!379>H^9=WJ?;POetjYkp=kvvVo)-HDVdHeV literal 3410 zcmaJDOHdohwMR4hB7QOgg!std1#4pq@MqU1#;go7W(^Y2$o8Vz)MU^!W?(cU&y298 z#Z@@#Y?03aEjdDhgs6SiD)Lsx$QBTQKuI838L1?NX^Uh@(@8qb zB$>1|X{E>2 zGbzTTtVz2`*$9P0cPx1-W)4HaE;ycADx4+n;h73vOIL6%p9COT2i@44{YZ;0p{`~z zS#L(*GUIV48%(O+q#BZqunF60JknOvQ>qe2vf0#aI#Rk>@EmydnDWg>${!P24&;xS z^2d&p_X@2C@-3#km&glkNv}z@AF1s-$fXswLB@wQasIv|bvh1o+7Ib;RCStua9908 zXD!kL3qEtb{YU!iI*{%-Uo$do2{~?|3BXxR&KrOyxr@3}LL|!;0^ujKo4>H0D z*e$#VyB8cn|3hoC2WJ`(285GBPcB(4pDsgQ_{Mi5i$R8dbkFYG4h+U6FxyUE8Q}GBlGZnp&h_J{#5l)1!gxLspBR-9s z@yNs!OVE6u3I$Oc864E0x+10^w;;*_RJjWKgR>uA92`Ook(uz+G>22WIW>VJVEC-A zpk|ZHa#!QA8P3G>O`^P@e3Vg@1#w={LyKADn5`11>99Ci9n{1|q7#$xsR@p(21(V) z&Ln2JYcV!aMQ3C2gm@jdjw*eF#qR4(so5~g-ipO1P%{o7n!u*GT7r4+qONCfQj(J5 zrA0;4N6ron4MmYfm`9FfnB!zV4Ut`d3w&16kxhkqR+UrL4L7%)^!?BA|Mmq2*8zoF zq7&%VMuCfnsukpYW|dm1c5hR&3RW4xvT6~jS_yuRTjyt3?zi+7{e{&E?ps%_OSRkO zkYcSF-nXsVRM)CaFc0wqYYk(;D$Rlq!Ebu|fNrZPo&YMoc~DFljtSW@}oGqt#<@d3H`L|QJsz#_bm7G+{5 zAi+g^{XjWzV6eC+NyTS6FO#U2vN|s*>43~{BsTLyB$vdpIh2XzE6#o zpRPyB{;r*o?U7yh7(V}`bo#R4AA9W`TfbWNv~LY=4jP`m_3^J8JsVnKcyp!H7A&`Q z5NxBqe5_+Vy6*(7A5s5$=K@W>e*ntq$J}2<_i2p%N1MPte@=xvsfWGcI`BIx>|`GG zVCAcGv3 zjXRG7QF9IBpqx?!{Gve(Dg3;_iz92IF0Mdi#V;9^4^q)!FLH3^QRX=0;<&UTWF?4k zH^<${@)9}Z9H+W*^oZ7U6*Y1kFUyL~lLwTBr|R*P+Cb(liiT`>xv_g>m6W?sRT~MP znO77^C2ty)JY3XP!lXr6SKA2LP5`-NRX+h;1auQXT8Y|2K!AYb1oRTnM*w+isWbs+ z2q0C0+&FwFah0-o0^VXCP=^Vqs{vA*+6&x+egXc{er|1gqt9T@;f-3GTxSiYqukoF z_VEU7Fg<0Df1`IRxEU-=?%pyyr(b){zVe(kJm#kBn6Z5aXbWu2Z;6}Y zuDcYvY_yJ5)W^!+&b6s^&0spq17}K1&+ajUIkRsAzTSdm$GPp?4H@mj75BqsZ}&!g zYi@I{@LB29PYiFoq8Z2H{u71Xo#1w`bmH9e1*2nhExIvoFr($J{z80bZhNjYFjBe{ zHM(LY#=C*N#mb%iC8lK~Wib6%ePC#J{*kyRmWHkv{o`a}v7l|H4Q9ODbF!fB@ zZ**;D<3eG4a})=P7pQySP^hfYm8gcAcw=j@`gd3N0Uo~6%;Ar$Ig~!--V*McUeMBC zZf+}g951)|%MGpXoYofGgK3qVCUZ}Njp``I5ktBbvgx@DF84}VnB_w*2h^O&3B0U} zsaku`RMPY-HRICmNW7Fl}YR4N5W>IgY%y0^e+Bx#1DC%A{%^tHJLUpqTVH(#@bG4Z0`8Vy Kk34Xg7XJ%GjB3mP diff --git a/backend/app/core/__pycache__/i18n.cpython-312.pyc b/backend/app/core/__pycache__/i18n.cpython-312.pyc index 18b715dc1496b903bf960ecf1ef714127c2382f3..a615e0d17a340f73534964c3a2fd6a1fbc601043 100644 GIT binary patch delta 1400 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 ORIWcF{dke6-~9qc?3cX& diff --git a/backend/app/core/__pycache__/validators.cpython-312.pyc b/backend/app/core/__pycache__/validators.cpython-312.pyc deleted file mode 100644 index 57a79366cb79d71b73f5a197481415fd592c4e53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3008 zcma)8UrZZE8lUm5jcveS{=^WPu=!h@68^afB}z~zAr7U1CR|A1nz|d$0-G4ynOy_+ zfrNCCV&_h3P}M@DK9EY4K+#E&E)S{Hhp4nts(T?vtIN&ZL!`Th^BV~#T`o`e&FmT_ zxk`5{?Ki)j`F-=vH{bWo>>o-?y$Bvh<#qi`5ki0CAB9J@!KMOW0TD!S42>WM3M1mU zhKO{>VKGK<8VBV(|LBieU6yp(ADhPkEFcYyV1h;*nnbWB6Nly;m5FpszGKryTw5bW zFe1M+;!Zn*E><$s+drhnbfTIG%A`p>&RkS8Q#2mM_8h!%!7IVLSpo~Zj;_nsoeL<3 zF2Bs-Ja`JGFLVBDPhso^G7FBN%Y5$F9)feIuobk*^c04(XcWUnxruaQ%g!Bp4HTzk z9T6vSeTqpDaf9>_FUVq20{Oy8SN8SMD z4#C;UIbf$=uoS`CC1%|%xO>E`dxge6vLE!`6c!qUc2HP2Aavgnx{X5jpwMj+y3Jho zD||y9L!{+Pe8cHMX7Sbz$XS$ij!LBUn%gWb^pdv1F8uet%;FhLLK)cQS;vfnynUqs zA?hHz<6g6@pg-d%=+8%)fEFsg9A#ay}d&*G8)z6 zW>d!Z(J#9Wj60>^Z?d-8m}44=&N>R>pWhoc1AQ+edlWB7fBPcz)l@COW^&GRZ@c#z>=Fno^TA%@R0e1YL%hDR7~p_ZjqhT9kpG5j{e zhZsK0a67~AFx(OJ(|VYJ?%<-13!Viuz(q9|l_2umQ85#JlcWEw@k$uEb0UmK0DLBG zg|UK%G3UrhGngCYZ%(#w1~$SDZB-$(l)Fm#Qi4v(OM_vZxgb0#%@BcRMN^tSHf|aQ z-$sb|+#Kb_LVY0q|EThH7<)k2Q4NH@9=?wkU5mz2>#fOKzU9DO|H}5CYC>NR+aa#0 z!+Ad_*y%Hh%m~JvRB&ZfOHA0VZOPwGLo+=+JigNt8aF)3X37e_@ z**xaaFp429Ud*8l7b>gxM9=BJx$-Ap{Ucxfx^Guj`lkz(S9~&^n_jC4E{A?BYrYp+ z=cx_i$I!sz%9;(AT;Y9OQg#z>NE}seyLr_@+jlIg7TUFU>5PRM-dgUmP;=YeRtp{O zxWCIn$BsYnTj)gip*>$;|3iDev**@^&RfI7AFf^eNWdq;vJ)&n5taoKmIV@)1>%;U z3M4EGBrFRgEDIzo3nVNHWLf4AHmS56TrIM2RREl|aotXE*T%c|fx|WqHiFAGZV7?Y zHf}!xZrixC3xntOM-M)Q!F&7beLR34UVX~HvaV-7R2jzEq7Av!Q~UR-z)ykjH|HL- zeKWis2(MR#vpulBI%R41tpnLUfb|EK&;D^|^Sp6y@P6I;p`Pq1=;UjjKfHM1QRTk1 zl6}wrS{8VQu&4IV5K0N!7Y39f`nKa(cRN~b_neTV?;IH9D^<+%j2~BxsAv7d!NLcn=>T-tCAKe<2N`U`gsEboS}-G~5e{no-o4 znpFA!$raU9R0UG`;HB^9V@kvM)4h$#YHsrK>TRl;jY>ETz+C;klAuOr_4Zh>iAm=M zyO^s_jl%>fzV7$b* zx7V1^;13DzGV2KBXCquQMbShmZc?~D(wa&m(FB2Vo|@2uaORI`X3$~Pvd5z7nqlzs zSW&D}EeA?uRE@%N(h=}g&FpxkWM3cN54uJd2&h~)5Bm7s3l9d?MlL<7ie^1e0?NYd z{Or=;vj6tb@`vkzBads}SUhuUo2bu4|IK$GqsqM-NcMO)VG`62r<6MqiK`P@B*MIr z$V7sqVjP!5B3Dyt%pNH*Ot{mdux;ZBLQjDvlP@I_F?w1+qDM`70iZaW=`cs)*ySe; z<$|Y;(F5WddhGOXIO?6XH=`Q}nnm)AH=7c2RfztLcd>wOr*{!3&@2H38bNvL0AyL< r^TPIaLnCawW}LZ?THbWhkAcGPU6JIF?->6DdHy9gV(&8qLQMA`3x@hT diff --git a/backend/app/core/config.py b/backend/app/core/config.py index d08760b..cb6770c 100755 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,6 +1,5 @@ import os -import json -from typing import Any, Optional, List +from typing import Any, Optional from pydantic_settings import BaseSettings, SettingsConfigDict from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession @@ -10,41 +9,38 @@ class Settings(BaseSettings): PROJECT_NAME: str = "Traffic Ecosystem SuperApp" VERSION: str = "1.0.0" API_V1_STR: str = "/api/v1" - DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true" + DEBUG: bool = False # --- Security / JWT --- - # Szigorúan .env-ből! - SECRET_KEY: str = os.getenv("SECRET_KEY", "NOT_SET_DANGER") + SECRET_KEY: str = "NOT_SET_DANGER" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap - # --- Database & Cache --- - DATABASE_URL: str = os.getenv("DATABASE_URL") - REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0") + # --- Initial Admin (ÚJ SZEKCIÓ) --- + # Ezeket a .env-ből fogja venni + INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu" + INITIAL_ADMIN_PASSWORD: str = "Admin123!" - # --- Email (Auto Provider) --- - EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto") - EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "info@profibot.hu") + # --- Database & Cache --- + DATABASE_URL: str + REDIS_URL: str = "redis://service_finder_redis:6379/0" + + # --- Email --- + EMAIL_PROVIDER: str = "auto" + EMAILS_FROM_EMAIL: str = "info@profibot.hu" EMAILS_FROM_NAME: str = "Profibot" - # SMTP & SendGrid (Szigorúan .env-ből) - SENDGRID_API_KEY: Optional[str] = os.getenv("SENDGRID_API_KEY") - SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST") - SMTP_PORT: int = int(os.getenv("SMTP_PORT", 587)) - SMTP_USER: Optional[str] = os.getenv("SMTP_USER") - SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD") + SENDGRID_API_KEY: Optional[str] = None + SMTP_HOST: Optional[str] = None + SMTP_PORT: int = 587 + SMTP_USER: Optional[str] = None + SMTP_PASSWORD: Optional[str] = None # --- External URLs --- - # .env-ben legyen átírva a .10-es IP-re! - FRONTEND_BASE_URL: str = os.getenv("FRONTEND_BASE_URL", "http://localhost:3000") + FRONTEND_BASE_URL: str = "http://localhost:3000" # --- Dinamikus Admin Motor --- async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any: - """ - Lekéri a paramétert a data.system_settings táblából. - Ezzel érjük el, hogy a kód újraírása nélkül, adminból lehessen - állítani a jutalom napokat, százalékokat, stb. - """ try: query = text("SELECT value_json FROM data.system_settings WHERE key_name = :key") result = await db.execute(query, {"key": key_name}) @@ -63,5 +59,4 @@ class Settings(BaseSettings): extra="ignore" ) - settings = Settings() \ No newline at end of file diff --git a/backend/app/core/i18n.py b/backend/app/core/i18n.py index b1fc0ef..3fcfe2d 100644 --- a/backend/app/core/i18n.py +++ b/backend/app/core/i18n.py @@ -1,3 +1,4 @@ +# /opt/docker/dev/service_finder/backend/app/core/i18n.py import json import os @@ -9,21 +10,44 @@ class LocaleManager: self._load() data = self._locales.get(lang, self._locales.get("hu", {})) + # Biztonságos bejárás a pontokkal elválasztott kulcsokhoz for k in key.split("."): - data = data.get(k, {}) + if isinstance(data, dict): + data = data.get(k, {}) + else: + return key # Ha elakadunk, adjuk vissza magát a kulcsot if isinstance(data, str): return data.format(**kwargs) return key def _load(self): - path = "backend/app/locales" # Konténeren belül: "/app/app/locales" - if not os.path.exists(path): path = "app/locales" + # A konténeren belül ez a biztos útvonal + possible_paths = [ + "/app/app/locales", + "app/locales", + "backend/app/locales" + ] + path = "" + for p in possible_paths: + if os.path.exists(p): + path = p + break + + if not path: + print("FIGYELEM: Nem található a locales könyvtár!") + return + for file in os.listdir(path): if file.endswith(".json"): lang = file.split(".")[0] - with open(os.path.join(path, file), "r", encoding="utf-8") as f: - self._locales[lang] = json.load(f) + try: + with open(os.path.join(path, file), "r", encoding="utf-8") as f: + self._locales[lang] = json.load(f) + except Exception as e: + print(f"Hiba a {file} betöltésekor: {e}") -locale_manager = LocaleManager() \ No newline at end of file +locale_manager = LocaleManager() +# Rövid alias a könnyebb használathoz +t = locale_manager.get \ No newline at end of file diff --git a/backend/app/core/rbac.py b/backend/app/core/rbac.py new file mode 100644 index 0000000..3400e54 --- /dev/null +++ b/backend/app/core/rbac.py @@ -0,0 +1,40 @@ +# /opt/docker/dev/service_finder/backend/app/core/rbac.py +from fastapi import HTTPException, Depends, status +from app.api.deps import get_current_user +from app.models.identity import User + +class RBAC: + def __init__(self, required_perm: str = None, min_rank: int = 0): + self.required_perm = required_perm + self.min_rank = min_rank + + async def __call__(self, current_user: User = Depends(get_current_user)): + # 1. Szuperadmin (Rank 100) mindent visz + if current_user.role == "SUPERADMIN": + return True + + # 2. Rang ellenőrzés (Hierarchia) + # Itt feltételezzük, hogy a role-okhoz rendelt rank-okat egy configból vesszük + user_rank = self.get_role_rank(current_user.role) + if user_rank < self.min_rank: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Ezen a hierarchia szinten ez a művelet nem engedélyezett." + ) + + # 3. Egyedi képesség ellenőrzés (Capabilities) + user_perms = current_user.custom_permissions.get("capabilities", []) + if self.required_perm and self.required_perm not in user_perms: + # Ha a sablonban sincs benne, akkor tiltás + if not self.check_role_template(current_user.role, self.required_perm): + raise HTTPException(status_code=403, detail="Nincs meg a specifikus jogosultságod.") + + return True + + def get_role_rank(self, role: str): + ranks = {"COUNTRY_ADMIN": 80, "REGION_ADMIN": 60, "MODERATOR": 40, "SALES": 20, "USER": 10} + return ranks.get(role, 0) + + def check_role_template(self, role: str, perm: str): + # Ide jön majd az RBAC_MASTER_CONFIG JSON betöltése + return False \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 92a3208..755cd18 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -4,6 +4,20 @@ import bcrypt from jose import jwt, JWTError from app.core.config import settings +# Master Book 5.0: RBAC Rank Definition Matrix +# Ezek a szintek határozzák meg a hozzáférést a Middleware szintjén. +RANK_MAP = { + "superadmin": 100, + "country_admin": 80, + "region_admin": 60, + "moderator": 40, + "sales": 20, + "user": 10, + "service": 15, + "fleet_manager": 25, + "driver": 5 +} + def verify_password(plain_password: str, hashed_password: str) -> bool: """Összehasonlítja a sima szöveges jelszót a hash-elt változattal.""" if not hashed_password: @@ -22,14 +36,23 @@ def get_password_hash(password: str) -> str: return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8") def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: - """Létrehozza a JWT access tokent.""" + """ + Létrehozza a JWT access tokent bővített RBAC adatokkal. + Várt kulcsok: sub (user_id), role, rank, scope_level, scope_id + """ to_encode = data.copy() if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - to_encode.update({"exp": expire}) + # Rendszer szintű metaadatok hozzáadása + to_encode.update({ + "exp": expire, + "iat": datetime.now(timezone.utc), + "iss": "service-finder-auth" + }) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt diff --git a/backend/app/db/__pycache__/__init__.cpython-312.pyc b/backend/app/db/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/db/__pycache__/base.cpython-312.pyc b/backend/app/db/__pycache__/base.cpython-312.pyc index dcf3c65a9985fc54d9f84dd1ccc35c887cdabbfa..ac879e93e6ae97840cd2da0f9f8fa3565c7c55e0 100644 GIT binary patch delta 740 zcmYk2J8u&~5XW~fB(Y=X?N<^%;s;4Ci2_0bi83URCJ@VZD7LYV^Kf42_RiWpJDA4Q zh{p1#poFhMnQjGgl>&(Z1jrQX@8oT|NLif=hO6$=)#xz`8lM%{J;0~YHGNU z!C!~l&VVIIf+b0krAUgUNgAUTYR$MAwno<2I$38MWP@c%mgPu}3uNFnT*bvId& z6vH^;mROmTS%p->JnG(MTVyMY=iF_!Lw1-+OtwpQ*&f-$s2?-q-}T=NKYqZG(IdP+ zkc)>xK=6!umS=dgG{6N00q@ILnAf03;X=lrzOmbu=RnO0z?_xQs!zS3)^$O~ zPN4_xk?*~xj*OlQ;Pt=?gq(h2wH&yvT!%BtOg%@&tNuW_#VM!;ZeqDu?~4Ff)#8@A zTa^qMIqbG*aOyj9=_RzO?ZVlG2VAsiM;Ri=zTIWu1?C;ORPVkLHm99{`d)(qmzgu} zSRQ?=Zu7m{s$4$epvE?M-tvTHYhC$)_oxl^4(LS${TJr`=pFvN%vaRL_^L)iBRRTf zJ!*g0*xh1!$2z=h>d2KFeb@i6o zx_Kqv)Jjq-rJK37&!Or14z+nvdrKN+jmoHI>^2{$5UV4eEBu_o7+;~%WCr0XzC>qZ tbaIJK#^`urOkrb!6n<+=B8E;Cerfzw{SU|J@i(+HF;xCX7x{fP@jnlF;GzHk delta 186 zcmaFExsjFcG%qg~0}x0b@5tQDIFV0+v0|cnx)>uv3PTE8j!3R(lxVJ46p&xZuE{Z( zgK;7w%j8*%I&zv!w>U$KQ;UL25=)A2@dV^&=9Lutq^6{&7Wrw4PkzL-N#T}wVnKmk zZhlH?PO)BkVs2(yW^!UlW`16g5YWOR;mOG?TBfo '{test_save}'") + print(f" ✅ Paraméteres fordítás: '{test_email}'") + else: + print(" ❌ A fordítás NEM működik (csak a kulcsot adta vissza).") + print(f" Ellenőrizd a /app/app/locales/hu.json elérhetőségét!") + except Exception as e: + print(f" ❌ Hiba a nyelvi motor futtatásakor: {e}") + + print("\n" + "="*40) + print("✅ DIAGNOSZTIKA KÉSZ") + print("="*40 + "\n") + +if __name__ == "__main__": + asyncio.run(diagnose()) \ No newline at end of file diff --git a/backend/app/locales/hu.json b/backend/app/locales/hu.json index b1d1eee..c0fc27f 100644 --- a/backend/app/locales/hu.json +++ b/backend/app/locales/hu.json @@ -1,7 +1,7 @@ { "email": { - "registration_subject": "Regisztráció - Service Finder", - "password_reset_subject": "Jelszó visszaállítás - Service Finder", + "reg_subject": "Regisztráció - Service Finder", + "pwd_reset_subject": "Jelszó visszaállítás - Service Finder", "reg_greeting": "Szia {first_name}!", "reg_body": "A regisztrációd befejezéséhez és a 'Privát Széfed' aktiválásához kattints az alábbi gombra:", "reg_button": "Fiók Aktiválása", @@ -9,6 +9,23 @@ "pwd_reset_greeting": "Szia!", "pwd_reset_body": "Jelszó-visszaállítási kérelem érkezett. Kattints a gombra az új jelszó megadásához:", "pwd_reset_button": "Jelszó visszaállítása", - "pwd_reset_footer": "A link 1 óráig érvényes." + "pwd_reset_footer": "A link 1 óráig érvényes.", + "link_fallback": "Ha a gomb nem működik, másolja be ezt a linket a böngészőjébe:" + }, + "COMMON": { + "SAVE": "Mentés", + "CANCEL": "Mégse", + "DELETE": "Törlés" + }, + "VEHICLE": { + "LICENSE_PLATE": "Rendszám", + "VIN": "Alvázszám", + "ADD_SUCCESS": "Jármű sikeresen hozzáadva: {name}", + "NOT_FOUND": "A jármű nem található." + }, + "COST": { + "AMOUNT": "Összeg", + "CURRENCY": "Pénznem", + "VAT": "ÁFA" } } \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 63d30ad..3b19ade 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,37 +1,32 @@ +# /opt/docker/dev/service_finder/backend/app/models/__init__.py from app.db.base_class import Base + from .identity import User, Person, Wallet, UserRole, VerificationToken from .organization import Organization, OrganizationMember -from .asset import Asset, AssetCatalog, AssetCost, AssetEvent +from .asset import ( + Asset, AssetCatalog, AssetCost, AssetEvent, + AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate +) from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType -from .gamification import UserStats, PointsLedger +from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger +from .system_config import SystemParameter +from .document import Document +from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty +from .history import AuditLog, VehicleOwnership -# Aliasok a kompatibilitás és a tiszta kód érdekében +# Aliasok Vehicle = Asset UserVehicle = Asset VehicleCatalog = AssetCatalog ServiceRecord = AssetEvent __all__ = [ - "Base", - "User", - "Person", - "Wallet", - "UserRole", - "VerificationToken", - "Organization", - "OrganizationMember", - "Asset", - "AssetCatalog", - "AssetCost", - "AssetEvent", - "Address", - "GeoPostalCode", - "GeoStreet", - "GeoStreetType", - "UserStats", - "PointsLedger", - "Vehicle", - "UserVehicle", - "VehicleCatalog", - "ServiceRecord" + "Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", + "Organization", "OrganizationMember", "Asset", "AssetCatalog", "AssetCost", + "AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate", + "Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "PointRule", + "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", + "SystemParameter", "Document", "SubscriptionTier", "OrganizationSubscription", + "CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership", + "Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord" ] \ No newline at end of file diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index 42da96aa9397f979d002f8f6f477bc2359fec1d5..b41676acd28914b1bc6e4cb5ebe4b87b4ac0c287 100644 GIT binary patch delta 898 zcma)(O-~a+7{_N8=nH+J<+T)ApuCiYB5I;WO7ws+F>z)gMsH3szdZAQo|)%AGe1M8vE=(iq8G?f`t_Q8 z>N!qk;E&^C^@yfWil$MT_M?6}fCeD2G%Mr`(jhcNhtV({K_fJSGJzg;Mrjsh=@=TL z<7k}bP>$wNo=%_%I*BHOnus$+3#bspQD>UYpqU`PVORqGrXM z1-gh9X$h4ez;U!B=M9`d%eW8Sw1E|W1JSK!LQfvQb6}*s1WT);Z;f;8ZxYvVO=37) zBm-d=JD6ghHAO5~SMUMBha$f5!mJstjVp$a+fU)0qp*nXc*ON94F`)@86RM0-E;Se zEuxP(W>w$tx#)RlST?>=uELN!fVj4Z2b*~r%RJMe=&LrlkLixV4B0-AB9A17hN89jn{= zOB=BjMqqAFSg%8RAIyNx7mn7MqyMTt1zZ8u8>p6D@-U% zDoiO96s8qs6lN9X+E818x7(|l+x`Lvk3`t8EQUE}gDO3w@TaGU+Xf~3#0+lu9}Yx< z2amXC1|Ja^5)teA6$5Lc*Yp_Pb3B`vZ0fG+_peV8t`Y8gta)u%RTLHG6?BD#_J#JK zeqV-APX0{dq6=R&j!16^1#8R#BS#qEiJ4@$U$+J|pmH5^jjcKpK?4E4#8(BmC6aWAK delta 271 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 ThupSF+v52DK*gkuzn%XB!o1-{ literal 7192 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 Qfa9uT>Ga<@{AGvy4|ylrO#lD@ diff --git a/backend/app/models/__pycache__/company.cpython-312.pyc b/backend/app/models/__pycache__/company.cpython-312.pyc deleted file mode 100755 index 3bdb19b054685caa59b2a7804990eb2e819ea970..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3321 zcmb_eO>7&-72YM6+$EP>aw+P^YAZQtVuP+NwJvPebrQw4WLb8_wyKn%TNsNKXDqF= zyJTjUa_C%WAcyv*oGjmzLjVfW!KWhVNktD0P@oW?1u9Kp1VK-|HL!Z|q3E00B|`~8 z0RvrtZ{NIm^Z0h=``*024r78Su^G;&w8&4sxps)?A-;h+Feu)%vW%t#qaEPqwg< zrsGELggFvx`Qo`W^dIAwpYBnxJ8v|E5M7kiz_0{5$srj zUHAM|y8&bAe$%T}5>%S0F3nSEX=!enS#MZAuGXS7BPqs%7bBoN5wKv0NH7veG?FM` zNGNHfT2duN)thy!!i(3h*Hi+$7q-DMjC`s8K!XMt+ z5~<=f9miUR^ONoqjKLx!eBWw1!OJ{bI??no@h5CV^YOpTV|SUIb9?}gN|xiCKMiMs z1AL)Q8mpEE7}J)m+WTgs-e>{@As@MB`85k+s=td@?V5wH`#wO{#a^%_lJl^E)rw9A z;7!U*41%C}(+pT{F+{X@Xj){&H%-F!i!j#FgpEjwDH3y~*}(eH^qM?R-ES`YHDWgy z5zN5GgviWO;czu!AwvzomVcH{RjZenPfo#91fB0N?R#CJf#Ulh0BGg$_WZ%n*k*cf z@NB#KRi>~mZ!SJ7hndUm8;7dCerEGhs8-t3hx+hFx>FAIiT0htk+F@n&RRHfc~2TV zEDWx%Jbl!e+#mnx?owE|+rIskHo7tO`PIGZa;VkXvxlXzz442?SHsf$fqaFlnH9`GVyM;@7P_24jv2r9}(utiBE19W40fv)r*n*+ZT z&V#McvuWRAV{m7FQlDQDY&t3;eW%o0TjI8XZeGSft6Ui*=U@|PA%>_KpDh^&D>)5f zk-P@-rOpxFBMqSZ7I~fdRG26-!Kw)QGz9Zj1s--z5rGB|4#`3UKZflqXdkVzKY&YArf!_ZObDB*+B6MIas_15a zTfW(B+UQY{6EI%SZW+!z@K6G1VII{z*2ZWI)&T$bUW(ZD6NneFOO7uT@-~>jJ$FCq zhS){tS1Tj0#5yv?;^simZ1=N(cA2P`VYh|C99XfXNw5BpX8ph-feGJ%se(PkwdgKN zd3Ct?`Y0#I5qUj1NjzrF zY4z1Pc3boE#fu%^RXFIL=R}>j(Z$?vg!yRe$ND^xZ8^9W_!A8%b}IxkCzVCN&ICV7 z6?(@Bv^=p4oi53PnMxSi1Df;$0^KmEQ4h)MHm`yFG>+f?RCzOMUEe16AOvgN`~?vI zN{XWRr7-_DVg9*r_PH?rrSQ&k;k$nq)T4A#99sYWdhn$6N$W^}!BJKftKx6bk-*+Z aqXXjU_30x4UPtE&;+q@NzXf>la`s<7d_i{r diff --git a/backend/app/models/__pycache__/core_logic.cpython-312.pyc b/backend/app/models/__pycache__/core_logic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f892bc3e7861b29923c3d56316eb9fba2bc397be GIT binary patch literal 2781 zcmb_eO>7fK6yCMhyX*B&Y$ren1yVqOOK~JrE)}J1fP_G64zbjB6?L`oOq?aVUNgG} zIVU4=YA*7exDU!5fe1vj1xM6HC!$B3s2+7BU2+@ySfyC%DC&=)U0y#yy?@DpUm7<( zS`H5%N~vpX5K!(2RM15t=n*37Q4%3hOVp)^Kx0%U(!$6^&Sv5Ri$%6lx^ZGo74=#_(UU{B6CYOlmo23ULPt=&@ zn5}wtvj5 z)q)@FO>J+i43#r%w_=qGwFVo2m_Xoi?<|O0!jqxn&5xh#KCmLM?K;*hcH#p|%1Wsn zf2%p(No1Fft_-ykN1Nw6$=uRAUw+iQ(8=sxx^X|&&J=D=vBO&lU#O}KUVJuw0`Zzv zm7#7bk+mBe+lS%}6#G&1Aj1xziSM7wcsPr21@R0DQ*kPZQ_-q> zgKO7K+akssa~+-t^?KE}?B~!j%f1E2IE3qhLLWix=$1e*c?%#AL&si;q152gAGA^*HYYmip`}mmUR}#yXdRenr6-${o$Q`v_1ocZhkh9TZut9=cJ@s3QYSaO zJlx71YF-WjC$%#tw*ZGncsby{ftmjkZ~zL8D5OUrJ-8s$Vl>&aQ-O`38n`+NhuHS? z0Jt4o*>j8%%P%pr>f!r#t2|K-TzG4D;E5fF#Ov6B){zWZ9>nV+gPp*Yn1jov<3iCx zl1UKSqs8Z7VJ9(AgEIl$6T|ekss!+;N{ z{O;QYDEtms+`JS55x09e0KSHq?Eugv5+xG!k{FanrV5Gm^$j)jfsB(LuRw&_>$1o# zGEIoS!<`JZ%%}O!VQc0L9Th%Y}3Zh z-{>RN;QUCc0XK$s1DkPpiPF#40MR?)zw39xe8{_5XwC&PTbPB?#leLk@qMIBgf{;EXj;Qf{6tMdMQbINr}a&@yR)f#l@4I a*j&Y)GAMmv5oYvZ>?rxl0HTXbfVu&7PZlNs delta 69 zcmZ3?JC~RDG%qg~0}%9YZqHo3k@qMIBm3sdEXj;K+=&GRdMQbINr}a&lRem6#qKi5 Ud|?r0^kM8M`N{yIi%fv}0Le@fOaK4? diff --git a/backend/app/models/__pycache__/history.cpython-312.pyc b/backend/app/models/__pycache__/history.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db83aa03d7668e8519002319d90818387a1d3eb1 GIT binary patch literal 2134 zcmb7FO>7%Q6yCMh-u2(cPMowvRZ~hKbpZ#|Du@apHBDoZK#Ej1NDI35Thv55rp4^I7Plo$vJ+auPHIV8 z)?{1J6cKqbD`ls(bU2S&89S?G!?|STY*iZx=Lsus7qo&X#Dt1Kl6M3m)53#z^h#|s zL=`|&L+Ds5UCKN~N=f0#i>}qQ9WF0AKCM&6lUICZI(4qh8$PX?Hs$FBmr=9sEYlX3 zmabf0;S$EURHYk!N#t2ZEyFim$6GfW;7iwgOf;YvYu$B+80_1gwLG|ev$&%cTI^Y#UY~&Se zyp=B%cz%_xn>CAGzU@$kmE@zhB1CulkjIl=ZJpW%mx$pT)$JJ1G?-}{tfk+8uahtx zLN}n~vf=5?rb#wci46qmY=4NV;DWj9})vNmvROWHFqr zH5sFhufxeexSnsY-dv#$2~mcv$`g(YX|oK@<=&oL!VR~D=Ku;XCufXCBmBWBsO8P{ z4qtAxSRM$W!JND;Fn5F}#nbICo=hC?CU?iD+bjF2k&fK0J}d^Q+4kIi;mAGtz7!mN ze|vNH{LjAa6+_MLG<#VIZ|OGqybX~qK}&FRV>@%rC@EpiE<&PhWqug zu-O3513GA7@C(;ZLF)f{hMKj%ehj!plI=IYHr~pWRIXfT64U?Ot@E^jGu?9Q9vg-2 z*cg~9E8=t#%~3EVkxjs`0K!*%xDq#Ahhh_eTyB8`dkw>;fCD|iPQb|CL~|0&TWB!b zLuIqK(M9#J(`fKdf@eUjb?WPWt3i3H=YYFcGB>K4H-UdG8 z1$ws|p{Q9I!!u|?1$>CJvwef7^S%RvvOV!1vKj9{Im){Iv1&W{_Wk1t`9tuWp>oo!yv??S{)x^64S6~ z4erH;>-lv?k(9%Y-vPr5(EeqzR$hZYFTG~L70Of$hP@wF^402c<&)^T`;^TA5C5FJ zb6}puMN#}iSbZ$4?g^_0!t{YKbs(HS5Kg|3GUB}0@m~lyzL-plC+_-x3owM=>>v1K BC?Ws= literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc index 14d73ba37b80585930a0ac71d6ff8b2ea6f46ed3..c791879d1b0bbfb16758dc3d01ecd5e77933ca72 100644 GIT binary patch delta 1975 zcma)7O-vhC5Z+zWM^)&C$8C1re`s(-_?04Y|E7-ovm<3L@k?LnWQ1B2} z&i+g&%X0zX%Dmg)Jr#Vt!FvtfSHTAj-f!?Vxmv@2NZ_XT7cd+g7|w-pvfKdJNa_x> z6_cCxokxf%7dYTS*L?E|oA5SdUM)Iv~u9$yZ*F{Kkg|1`unAk#l!55w6GJvfpwgF7jcEAy_ioS`e4g}09?FRUUrNCdk}C&`9k!u`;MB-d9<~rRXMJxDa}5k%5*9vrBuy3D=#UrxIB3` z0b5v7Rl59{>}2Tq{KDPlB6!l zWS$*!w({d_$ayNG`B$jCAX5tJxRh8-ON(-)E|vx=5nH8az?tS$V-WLvT)rd6HCy>Z zA=HiwInlr$ORK5m@;n^ivZAW6WtCcy^&(tHm_TqNoa4AwPP5#V;&C~pnN|G+e0qc! z`uq|cPLi&&kDY<$BgLiX7y%QfUgO!X&PNM{Q1h3T_1jhbJwA=tLl-uK*+ z4^Hlw>I&iG>mP0}=EKonO!ehvfl**f5T0}F58-r1w!qDXesxe$P+eA|KJ36@LvRoq zG1>%i}k2MQmt`@ahkJ4+c7eFpHok^A<{d94B&7(f3-nn`PY5Ge_6(JGe$Ddr2YKiD`$OeC%x&HD#QDsTmO(qg z#|S-ves2S4)~kb4R|kiNMP9Qd6GVoqE9+gPSc^7#rR#-{K}N;;*9rw^4j^0#AYKO0 z%&FupIYDixNU>YKW=pwz*b`smXxOW0gfIoK`UwDrEgla0rSE}D{|TX?*hDWe>i>uz zV0-@lZvAJa=RrR~-QVG()ocUHNs^Asm+2!Gt2yPq{~n>XLg z?0fHfd;je3{k|uX;*~Q0gCZPk{NUk4NkFN#sA-TMN;~ z?L8YEa*SO72D^dq62fVK1@=1@&F>Tit^KCR^WD;u%)3uzQ zP-#627jap9H?VY=eidM@ykls3KC7*1dVH^AbD=}!@nG+Gqa94Qa(hi>ik{9GOhmF%)zeS}+T18P+>Mapp9N$5 z3n{?A4H}a5LqxaS&Bl0t=u0xnzYmQi;z+le%gUv-`=3cRT~}5$x}H^DXLoo<`2386 zM7H$%Q(w(Kn5_vgsrfp{=+;6_fT1=NCU26hp_+hWE#V=TaRr82r<OHUI~6z+6}LMiREOiLfrQYgw2pn=N1RF6uL-ZDY`eu?xBB*H1xJ&bX%~MvW%KYBfR`ehk30N0m6h zs&uoWn-$%n(k+T!jlR=-Cnvk)7~&L%Rp#UvvZ7_0N0Uq?WkVr`M`^lx&93-uvQLg7 z#|4`60?qZW1{lEPYWC>TcMkEg1HGcTHTPNO5o&=Q`y8@QsEyqzRj5(5A9Zpc;FPBZ zE|p$SbioJoDy~7|8ZkBjo>cXb<~g@=NX;sPM{cXqTO?hFQ4eZSw#>HHDltAh`k$lr zpTo7Caq4j{5e`4?xY{ef4X-Pdc{U@j1wEo}>0(RumH|97E)j-RYk`dz`!E>I7A zpdr^NZ*{0!Vw?VAdnC3QwB%Zq`m1TbzYTrUC40$niga*#aF-IxfiNFV#aE8WT^F^% z>EkkK@jP`*ekR9&zjqwnRUn#Nj)&88iSVKzn$KF! zMN$w7iAY+kiY%oPD=fnfqe7$*E2bdbbJz(rt$z$gK-nUEnch8T8)x{oGVCp$<80;E2n_;8eHge`sP11MEppzh>0`RU4dclT_Kfb(;N2L2Zn@UaBicMq&7qiiw0|74BAr}; zBJ&`TT7(wjl7^Mu#gkO3^bli3-{KNT#f3X?1E=IaEBO%VEWNee)DZQq=R`x|NsKa3 Mbs6g~4GvQAZ&^4JKL7v# delta 1211 zcmZ`(?MqWp9NyiX)4lKRMep65bDPtaHBHxx78ugfNQ5E_?-w1Ot6rH??w!JGRChg-ZnlT++_Ay(PcD9b!6C( zopQ8_Ze4Rwj#iPwqioRT(H%~_%Lef^)`LSPYoC+)XchfcC*^1r8>!)W?tu-B5^bUT#Lt~8|23=}J zmKrXiaZ!RX)C8-}z}6*RkKI;d5a_lwdR)`Z1T83l-K@XZt}(47diCWy4&V+|xY{@g z;K(>kRtIS}GH=lHbROg&+>tqP|4{H{vp-(VpZvcckC@}>E~0m%5Nbi;QY$@b@tDR& z{$YDGHi}}UHa$;oA)aW*zsza}-0Cj~;4Xj<0RH^4ad_-0Q?5tSbY>}=-+EL(&eWeJ z!e}CeS<4KU0+Sud>S8uin8;=3@ng%n!#=gJl*=oHiKzu7Z4Fra8Kw)b*rIhm%mxpS zAKIE=#x>zrwiwL30(RSr{?A_Fe)|Tuo_KQQgSTgOM00Q5v9}qSLHxowR6Gsc%t(=! zB}M5bKNPrC$|-;_z$`$TzzUyPTujcB_E|M4BPo}c^M&M?Dk-B0ufl^(00;n_2N(i? z>#Udnh5;G~lwAwTOeQCRyhqGpT#NJp(DbEe0X}iSOLuJ6G)=4Sp@aA8OU?YXATM diff --git a/backend/app/models/__pycache__/system_config.cpython-312.pyc b/backend/app/models/__pycache__/system_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b868ff4c4a5a849d9ac864b278192443f78300df GIT binary patch literal 1251 zcmah}O>7)B6!y%{|L!K)kcNr_tWcpDp>CuKaY9I>EmeaOkdi$xQsjEq&N8iMW@FE! zY)^|+ap07~q2-p`T5S+Lax7XQ4qQ^PQdvs~Ax^!Orf}$~_RJ``7-=Y2jEWyj+!}f4wt2K3t)gX3L-7gB0bQfQc#MFz=+JijI6+l?7-Gk zj82`%4O|WA;3B}K8vq-`9h3^K!0Va5^+*BV7S_3hW>GAxwUmW%M;Oa%AFT@eQk;?w zVZvTY5=u}kobxCp>tRHMv6;nfUlYa>;-p<@7S@`8!za?Y1qcW<3<4c%K?&=DflJs3 zwZPN?v51XLZ0=jfNjZv@-Np7mD|A!@cJ|$YQQ*MKXO<=B4P>EI3o1ERm0IOvZYt+o z$^9EQtvK0dBwIOJLE_q#Q-#utb}#||H@;sz&i|#}ls_$|*Lpl9(Z`6Ph@^yNm!*PL7TCjU0#_KY) zMNu75)V(s?i~U9}Eo%RX6c%rPN+Ki-rL^wrqTFR+gjf$=B|Tw=F(#iqIxCa=z%L0a zi^Hvqu$siei8D%3ixOXBbCQrJZDX>DGMY-)N#zx|p2S2-PI1Dv34@bS;Usa^mFtmJ ze@-|M#o9F=pkn6 zHi(kk22+ninA?cT+->E%A&01)hTBAxG2v|%cGED4nIi|8o6n1rYr-qi#1N%-!J(wV zv-%?HcJnTCC6q5tO4?51&9Kwx_E<$}D5KWrUr2KU{H5!r^Vk9>7Wyl{&cC{A9h^Ab zU;V?K-m!Pr@6C2%^ zJH}|{O#jk&_T=sPJEumoi~SGBwU@tfqn;Q?B8^1K+ zsCN1DWp?@~u~ok;JibK{RcGHT%BHf-3zZizFVhRp66P~TkOIgBz> AhX4Qo literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/vehicle.cpython-312.pyc b/backend/app/models/__pycache__/vehicle.cpython-312.pyc deleted file mode 100644 index 775573c8e0bb5c4db7bb3a8e39e284ff273eb93f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6059 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 EU->q_H2?qr diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py index 7d7d83c..783dd76 100644 --- a/backend/app/models/asset.py +++ b/backend/app/models/asset.py @@ -1,133 +1,128 @@ import uuid -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Text from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.sql import func from app.db.base_class import Base class AssetCatalog(Base): - """Központi jármű katalógus (Admin/Bot által tölthető)""" __tablename__ = "vehicle_catalog" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) make = Column(String, index=True, nullable=False) model = Column(String, index=True, nullable=False) generation = Column(String) year_from = Column(Integer) year_to = Column(Integer) - vehicle_class = Column(String) # land, sea, air + vehicle_class = Column(String) fuel_type = Column(String) engine_code = Column(String) - + factory_data = Column(JSON, server_default=text("'{}'::jsonb")) assets = relationship("Asset", back_populates="catalog") class Asset(Base): - """A Jármű Identitás (Digital Twin törzsadatok)""" __tablename__ = "assets" __table_args__ = {"schema": "data"} - id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) vin = Column(String(17), unique=True, index=True, nullable=False) license_plate = Column(String(20), index=True) name = Column(String) year_of_manufacture = Column(Integer) - catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id")) - - # Nemzetközi mutatók - quality_index = Column(Numeric(3, 2), default=1.00) - system_mileage = Column(Integer, default=0) - mileage_unit = Column(String(10), default="km") # Nemzetközi: km, miles, hours - is_verified = Column(Boolean, default=False) status = Column(String(20), default="active") - created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) catalog = relationship("AssetCatalog", back_populates="assets") + financials = relationship("AssetFinancials", back_populates="asset", uselist=False) + telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False) assignments = relationship("AssetAssignment", back_populates="asset") events = relationship("AssetEvent", back_populates="asset") costs = relationship("AssetCost", back_populates="asset") + reviews = relationship("AssetReview", back_populates="asset") + ownership_history = relationship("VehicleOwnership", back_populates="vehicle") + +class AssetFinancials(Base): + __tablename__ = "asset_financials" + __table_args__ = {"schema": "data"} + id = Column(Integer, primary_key=True) + asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True) + acquisition_price = Column(Numeric(18, 2)) + acquisition_date = Column(DateTime) + financing_type = Column(String) + residual_value_estimate = Column(Numeric(18, 2)) + asset = relationship("Asset", back_populates="financials") + +class AssetTelemetry(Base): + __tablename__ = "asset_telemetry" + __table_args__ = {"schema": "data"} + id = Column(Integer, primary_key=True) + asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True) + current_mileage = Column(Integer, default=0) + mileage_unit = Column(String(10), default="km") + vqi_score = Column(Numeric(5, 2), default=100.00) + dbs_score = Column(Numeric(5, 2), default=100.00) + asset = relationship("Asset", back_populates="telemetry") + +class AssetReview(Base): + __tablename__ = "asset_reviews" + __table_args__ = {"schema": "data"} + id = Column(Integer, primary_key=True) + asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) + user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) + overall_rating = Column(Integer) + criteria_scores = Column(JSON, server_default=text("'{}'::jsonb")) + comment = Column(Text) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + asset = relationship("Asset", back_populates="reviews") class AssetAssignment(Base): - """Birtoklás követése (Kié a jármű és mettől meddig)""" __tablename__ = "asset_assignments" __table_args__ = {"schema": "data"} - id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) - assigned_at = Column(DateTime(timezone=True), server_default=func.now()) released_at = Column(DateTime(timezone=True), nullable=True) - status = Column(String(30), default="active") - notes = Column(String) - asset = relationship("Asset", back_populates="assignments") - organization = relationship("Organization", back_populates="assets") class AssetEvent(Base): - """Élettörténeti események (Szerviz, km-óra állások, balesetek)""" __tablename__ = "asset_events" __table_args__ = {"schema": "data"} - id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) - - event_type = Column(String(50), nullable=False) - event_date = Column(DateTime(timezone=True), server_default=func.now()) - + event_type = Column(String(50), nullable=False) recorded_mileage = Column(Integer) - description = Column(String) data = Column(JSON, server_default=text("'{}'::jsonb")) - asset = relationship("Asset", back_populates="events") class AssetCost(Base): - """ - Költségkezelő modell: Bruttó/Nettó/ÁFA és deviza támogatás. - """ __tablename__ = "asset_costs" __table_args__ = {"schema": "data"} - id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) - - cost_type = Column(String(50), nullable=False) # fuel, service, tax, insurance, toll - - # Pénzügyi adatok - amount = Column(Numeric(18, 2), nullable=False) # Bruttó összeg - net_amount = Column(Numeric(18, 2)) # Nettó összeg - vat_amount = Column(Numeric(18, 2)) # ÁFA érték - vat_rate = Column(Numeric(5, 2)) # ÁFA kulcs (pl. 27.00) - - # Nemzetközi deviza kezelés - currency = Column(String(3), default="HUF") # Riportálási deviza - original_currency = Column(String(3)) # Számla eredeti devizája - exchange_rate_at_cost = Column(Numeric(18, 6)) # Rögzítéskori árfolyam - + driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) + cost_type = Column(String(50), nullable=False) + amount_local = Column(Numeric(18, 2), nullable=False) + currency_local = Column(String(3), nullable=False) + amount_eur = Column(Numeric(18, 2), nullable=True) + net_amount_local = Column(Numeric(18, 2)) + vat_rate = Column(Numeric(5, 2)) + exchange_rate_used = Column(Numeric(18, 6)) date = Column(DateTime(timezone=True), server_default=func.now()) - description = Column(String) - invoice_id = Column(String) mileage_at_cost = Column(Integer) - data = Column(JSON, server_default=text("'{}'::jsonb")) - asset = relationship("Asset", back_populates="costs") class ExchangeRate(Base): - """Napi árfolyamok tárolása (ECB/MNB adatok alapján)""" __tablename__ = "exchange_rates" __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) base_currency = Column(String(3), default="EUR") - target_currency = Column(String(3), nullable=False) + target_currency = Column(String(3), unique=True) rate = Column(Numeric(18, 6), nullable=False) - rate_date = Column(DateTime(timezone=False), index=True) - provider = Column(String(50), default="ECB") - updated_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file + rate_date = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/models/company.py b/backend/app/models/company.py deleted file mode 100755 index 9bdb0ee..0000000 --- a/backend/app/models/company.py +++ /dev/null @@ -1,63 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func -from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID -from app.db.base import Base -import enum - -# A Python enum marad, de a Column definíciónál pontosítunk -class CompanyRole(str, enum.Enum): - OWNER = "owner" - MANAGER = "manager" - DRIVER = "driver" - -class Company(Base): - __tablename__ = "companies" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - name = Column(String, nullable=False) - tax_number = Column(String, nullable=True) - subscription_tier = Column(String, default="free") - owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - - members = relationship("CompanyMember", back_populates="company", cascade="all, delete-orphan") - assignments = relationship("VehicleAssignment", back_populates="company") - -class CompanyMember(Base): - __tablename__ = "company_members" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - company_id = Column(Integer, ForeignKey("data.companies.id"), nullable=False) - user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - - # JAVÍTÁS: Kifejezetten megadjuk a natív Postgres típust - role = Column( - PG_ENUM('owner', 'manager', 'driver', name='companyrole', schema='data', create_type=False), - nullable=False - ) - - can_edit_service = Column(Boolean, default=False) - can_see_costs = Column(Boolean, default=False) - is_active = Column(Boolean, default=True) - - company = relationship("Company", back_populates="members") - user = relationship("User") - -class VehicleAssignment(Base): - __tablename__ = "vehicle_assignments" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - company_id = Column(Integer, ForeignKey("data.companies.id"), nullable=False) - vehicle_id = Column(UUID(as_uuid=True), ForeignKey("data.vehicles.id"), nullable=False) - driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - - start_date = Column(DateTime(timezone=True), server_default=func.now()) - end_date = Column(DateTime(timezone=True), nullable=True) - notes = Column(String, nullable=True) - - company = relationship("Company", back_populates="assignments") - vehicle = relationship("Vehicle") # Itt már a Vehicle-re hivatkozunk - driver = relationship("User", foreign_keys=[driver_id]) \ No newline at end of file diff --git a/backend/app/models/core_logic.py b/backend/app/models/core_logic.py index 9c80bc2..30291b4 100755 --- a/backend/app/models/core_logic.py +++ b/backend/app/models/core_logic.py @@ -1,7 +1,8 @@ from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, JSON, Numeric from sqlalchemy.orm import relationship from sqlalchemy.sql import func -from app.db.base import Base +# JAVÍTVA: Import közvetlenül a base_class-ból +from app.db.base_class import Base class SubscriptionTier(Base): __tablename__ = "subscription_tiers" @@ -39,4 +40,4 @@ class ServiceSpecialty(Base): name = Column(String, nullable=False) slug = Column(String, unique=True) - parent = relationship("ServiceSpecialty", remote_side=[id], backref="children") + parent = relationship("ServiceSpecialty", remote_side=[id], backref="children") \ No newline at end of file diff --git a/backend/app/models/document.py b/backend/app/models/document.py index ce7942e..4f9731a 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -2,7 +2,8 @@ from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import func import uuid -from app.db.base import Base +# JAVÍTVA: Közvetlenül a base_class-ból importálunk, nem a base-ből! +from app.db.base_class import Base class Document(Base): __tablename__ = "documents" diff --git a/backend/app/models/email_log.py b/backend/app/models/email_log.py deleted file mode 100755 index 75f50bd..0000000 --- a/backend/app/models/email_log.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey -from sqlalchemy.sql import func -from app.db.base import Base - -class EmailLog(Base): - __tablename__ = "email_logs" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, nullable=True) # Hozzáadva - recipient = Column(String, index=True) # Hozzáadva - email = Column(String, index=True) - email_type = Column(String) # Frissítve a kódhoz - type = Column(String) # Megtartva a kompatibilitás miatt - provider_id = Column(Integer) # Hozzáadva - status = Column(String) # Hozzáadva - sent_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/app/models/email_provider.py b/backend/app/models/email_provider.py deleted file mode 100755 index 42f9a09..0000000 --- a/backend/app/models/email_provider.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, JSON, Float -from app.db.base import Base - -class EmailProviderConfig(Base): - __tablename__ = "email_provider_configs" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - name = Column(String(50), unique=True) # Pl: SendGrid_Main, Office365_Backup - provider_type = Column(String(20)) # SENDGRID, SMTP, MAILGUN - priority = Column(Integer, default=1) # 1 = legfontosabb - - # JSON-ban tároljuk a paramétereket (host, port, api_key, user, stb.) - settings = Column(JSON, nullable=False) - - is_active = Column(Boolean, default=True) - - # Failover figyelés - fail_count = Column(Integer, default=0) - max_fail_threshold = Column(Integer, default=3) # Hány hiba után kapcsoljon le? - success_rate = Column(Float, default=100.0) # Statisztika az adminnak diff --git a/backend/app/models/email_system.py b/backend/app/models/email_system.py deleted file mode 100755 index 9a0b13a..0000000 --- a/backend/app/models/email_system.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric -from sqlalchemy.sql import func -from app.db.base import Base - -class EmailProvider(Base): - __tablename__ = 'email_providers' - __table_args__ = {'schema': 'data'} - id = Column(Integer, PRIMARY KEY=True) - name = Column(String(50), nullable=False) - priority = Column(Integer, default=1) - provider_type = Column(String(10), default='SMTP') - host = Column(String(255)) - port = Column(Integer) - username = Column(String(255)) - password_hash = Column(String(255)) - is_active = Column(Boolean, default=True) - daily_limit = Column(Integer, default=300) - current_daily_usage = Column(Integer, default=0) - -class EmailLog(Base): - __tablename__ = 'email_logs' - __table_args__ = {'schema': 'data'} - id = Column(Integer, PRIMARY KEY=True) - user_id = Column(Integer, ForeignKey('data.users.id'), nullable=True) - email_type = Column(String(50)) - recipient = Column(String(255)) - provider_id = Column(Integer, ForeignKey('data.email_providers.id')) - status = Column(String(20)) - sent_at = Column(DateTime(timezone=True), server_default=func.now()) - error_message = Column(Text) diff --git a/backend/app/models/email_template.py b/backend/app/models/email_template.py deleted file mode 100755 index 67ed983..0000000 --- a/backend/app/models/email_template.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import Column, Integer, String, Text, Enum -import enum -from app.db.base import Base - -class EmailType(str, enum.Enum): - REGISTRATION = "REGISTRATION" - PASSWORD_RESET = "PASSWORD_RESET" - GDPR_NOTICE = "GDPR_NOTICE" - -class EmailTemplate(Base): - __tablename__ = "email_templates" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - type = Column(Enum(EmailType), unique=True, index=True) - subject = Column(String(255), nullable=False) - body_html = Column(Text, nullable=False) # Adminról szerkeszthető HTML tartalom diff --git a/backend/app/models/expense.py b/backend/app/models/expense.py deleted file mode 100755 index 86168c8..0000000 --- a/backend/app/models/expense.py +++ /dev/null @@ -1,50 +0,0 @@ -import enum -from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean, Date, JSON -from sqlalchemy.sql import func -from app.db.base import Base - -# Költség Kategóriák -class ExpenseCategory(str, enum.Enum): - PURCHASE_PRICE = "PURCHASE_PRICE" # Vételár - TRANSFER_TAX = "TRANSFER_TAX" # Vagyonszerzési illeték - ADMIN_FEE = "ADMIN_FEE" # Eredetiség, forgalmi, törzskönyv - VEHICLE_TAX = "VEHICLE_TAX" # Gépjárműadó - INSURANCE = "INSURANCE" # Biztosítás - REFUELING = "REFUELING" # Tankolás - SERVICE = "SERVICE" # Szerviz / Javítás - PARKING = "PARKING" # Parkolás - TOLL = "TOLL" # Autópálya matrica - FINE = "FINE" # Bírság - TUNING_ACCESSORIES = "TUNING_ACCESSORIES" # Extrák - OTHER = "OTHER" # Egyéb - -class VehicleEvent(Base): - __tablename__ = "vehicle_events" - __table_args__ = {"schema": "data"} - - id = Column(Integer, primary_key=True, index=True) - vehicle_id = Column(Integer, ForeignKey("data.user_vehicles.id"), nullable=False) - - # Esemény típusa - event_type = Column(Enum(ExpenseCategory, schema="data", name="expense_category_enum"), nullable=False) - - date = Column(Date, nullable=False) - - # Kilométeróra (KÖTELEZŐ!) - odometer_value = Column(Integer, nullable=False) - odometer_anomaly = Column(Boolean, default=False) # Ha csökkenést észlelünk, True lesz - - # Pénzügyek - cost_amount = Column(Integer, nullable=False, default=0) # HUF - - # Leírás és Képek - description = Column(String, nullable=True) - image_paths = Column(JSON, nullable=True) # Lista a feltöltött képek (számla, fotó) útvonalairól - - # Kapcsolat a szolgáltatóval - # Ha is_diy=True, akkor a user maga csinálta. - # Ha is_diy=False és service_provider_id=None, akkor ismeretlen helyen készült. - is_diy = Column(Boolean, default=False) - service_provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/models/history.py b/backend/app/models/history.py index 1df733e..8d4d21d 100755 --- a/backend/app/models/history.py +++ b/backend/app/models/history.py @@ -1,63 +1,30 @@ -# /opt/service_finder/backend/app/models/history.py - from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text from sqlalchemy.orm import relationship from sqlalchemy.sql import func -from app.db.base import Base +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from app.db.base_class import Base -# --- 1. Jármű Birtoklási Előzmények (Ownership History) --- -# Ez a tábla mondja meg, kié volt az autó egy adott időpillanatban. -# Így biztosítjuk, hogy a régi tulajdonos adatai védve legyenek az újtól. class VehicleOwnership(Base): __tablename__ = "vehicle_ownerships" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) - - # Kapcsolatok - vehicle_id = Column(Integer, ForeignKey("data.user_vehicles.id"), nullable=False) + vehicle_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False) user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - - # Időszak - start_date = Column(Date, nullable=False, default=func.current_date()) # Mikor került hozzá - end_date = Column(Date, nullable=True) # Ha NULL, akkor ő a jelenlegi tulajdonos! - - # Jegyzet (pl. adásvételi szerződés száma) + start_date = Column(Date, nullable=False, default=func.current_date()) + end_date = Column(Date, nullable=True) notes = Column(Text, nullable=True) - # SQLAlchemy kapcsolatok (visszahivatkozások a fő modellekben kellenek majd) - vehicle = relationship("UserVehicle", back_populates="ownership_history") - user = relationship("User", back_populates="owned_vehicles") + vehicle = relationship("Asset", back_populates="ownership_history") + user = relationship("User", back_populates="ownership_history") - -# --- 2. Audit Log (A "Fekete Doboz") --- -# Minden kritikus módosítást itt tárolunk. Ez a rendszer "igazságügyi naplója". class AuditLog(Base): __tablename__ = "audit_logs" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) - - # KI? (A felhasználó, aki a műveletet végezte) user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) - - # MIT? (Milyen objektumot érintett?) - target_type = Column(String, index=True) # pl. "vehicle", "cost", "user_profile" - target_id = Column(Integer, index=True) # pl. az autó ID-ja - - # HOGYAN? - action = Column(String, nullable=False) # CREATE, UPDATE, DELETE, LOGIN_FAILED, EXPORT_DATA - - # RÉSZLETEK (Mi változott?) - # Pl: {"field": "odometer", "old_value": 150000, "new_value": 120000} <- Visszatekerés gyanú! + target_type = Column(String, index=True) + target_id = Column(String, index=True) + action = Column(String, nullable=False) changes = Column(JSON, nullable=True) - - # BIZTONSÁG - ip_address = Column(String, nullable=True) # Honnan jött a kérés? - user_agent = Column(String, nullable=True) # Milyen böngészőből? - - # MIKOR? timestamp = Column(DateTime(timezone=True), server_default=func.now()) - - # Kapcsolat (Opcionális, csak ha le akarjuk kérdezni a user adatait a logból) user = relationship("User") \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index 33d5f61..9d53ffc 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -12,6 +12,7 @@ class UserRole(str, enum.Enum): service = "service" fleet_manager = "fleet_manager" driver = "driver" + superadmin = "superadmin" # Hozzáadva a biztonság kedvéért class Person(Base): __tablename__ = "persons" @@ -51,34 +52,37 @@ class User(Base): region_code = Column(String, default="HU") is_deleted = Column(Boolean, default=False) person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True) + preferred_language = Column(String(5), default="hu") + preferred_currency = Column(String(3), default="HUF") + timezone = Column(String(50), default="Europe/Budapest") - person = relationship("Person", back_populates="users") - wallet = relationship("Wallet", back_populates="user", uselist=False) - - # Itt a trükk: csak a string hivatkozás marad, így nincs import hiba, - # de a SQLAlchemy tudni fogja, hogy a UserStats-ra gondolunk. - stats = relationship("UserStats", back_populates="user", uselist=False) - - owned_organizations = relationship("Organization", back_populates="owner", lazy="select") + # RBAC & SCOPE mezők (Visszaállítva a DB sémához) + scope_level = Column(String(30), server_default="individual") + scope_id = Column(String(50)) + custom_permissions = Column(JSON, server_default=text("'{}'::jsonb")) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + # Kapcsolatok + person = relationship("Person", back_populates="users") + wallet = relationship("Wallet", back_populates="user", uselist=False) + stats = relationship("UserStats", back_populates="user", uselist=False) + ownership_history = relationship("VehicleOwnership", back_populates="user") + owned_organizations = relationship("Organization", back_populates="owner") + class Wallet(Base): - """Kétoszlopos pénztárca: Coin (Szerviz) és Kredit (Prémium).""" __tablename__ = "wallets" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("data.users.id"), unique=True) coin_balance = Column(Numeric(18, 2), default=0.00) credit_balance = Column(Numeric(18, 2), default=0.00) currency = Column(String(3), default="HUF") - user = relationship("User", back_populates="wallet") class VerificationToken(Base): __tablename__ = "verification_tokens" __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True, index=True) token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False) user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False) diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index c931de6..6279661 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -20,16 +20,16 @@ class Organization(Base): __table_args__ = {"schema": "data"} id = Column(Integer, primary_key=True, index=True) - - # ÚJ MEZŐ: Egységes címkezelés (GeoService hibrid) address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True) - # --- NÉVKEZELÉS --- - full_name = Column(String, nullable=False) # Teljes hivatalos név - name = Column(String, nullable=False) # Rövidített cégnév - display_name = Column(String(50)) # Alkalmazáson belüli rövidítés + full_name = Column(String, nullable=False) + name = Column(String, nullable=False) + display_name = Column(String(50)) - # --- ATOMIZÁLT CÍMKEZELÉS (Kompatibilitási réteg) --- + default_currency = Column(String(3), default="HUF") + country_code = Column(String(2), default="HU") + language = Column(String(5), default="hu") + address_zip = Column(String(10)) address_city = Column(String(100)) address_street_name = Column(String(150)) @@ -39,9 +39,7 @@ class Organization(Base): address_stairwell = Column(String(20)) address_floor = Column(String(20)) address_door = Column(String(20)) - country_code = Column(String(2), default="HU") - # --- ÜZLETI ADATOK --- tax_number = Column(String(20), unique=True, index=True) reg_number = Column(String(50)) @@ -65,7 +63,7 @@ class Organization(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) - # Kapcsolatok + # String alapú hivatkozás a körkörös import ellen assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan") members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") owner = relationship("User", back_populates="owned_organizations") @@ -76,13 +74,9 @@ class OrganizationMember(Base): id = Column(Integer, primary_key=True, index=True) organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False) user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False) - role = Column(String, default="driver") # owner, manager, driver, service_staff + role = Column(String, default="driver") - # JAVÍTVA: Jogosultságok JSONB mezője (can_add_asset, etc.) permissions = Column(JSON, server_default=text("'{}'::jsonb")) organization = relationship("Organization", back_populates="members") - user = relationship("app.models.identity.User") # Visszamutató kapcsolat a felhasználóra - -# Kompatibilitási réteg -Organization.vehicles = Organization.assets \ No newline at end of file + user = relationship("User") # Egyszerűsített string hivatkozás \ No newline at end of file diff --git a/backend/app/models/system.py b/backend/app/models/system.py new file mode 100644 index 0000000..d202a64 --- /dev/null +++ b/backend/app/models/system.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, String, JSON, DateTime, Boolean +from sqlalchemy.sql import func +from app.db.base_class import Base + +class SystemParameter(Base): + __tablename__ = "system_parameters" + __table_args__ = {"schema": "data", "extend_existing": True} + + key = Column(String, primary_key=True, index=True, nullable=False) + value = Column(JSON, nullable=False) + is_active = Column(Boolean, default=True) + description = Column(String) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) \ No newline at end of file diff --git a/backend/app/models/system_config.py b/backend/app/models/system_config.py new file mode 100644 index 0000000..9a1be7c --- /dev/null +++ b/backend/app/models/system_config.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, String, JSON, Integer, Boolean, DateTime, func +from app.db.base_class import Base + +class SystemParameter(Base): + """ + Globális rendszerbeállítások (A meglévő data.system_parameters tábla alapján). + """ + __tablename__ = "system_parameters" + __table_args__ = {"schema": "data"} + + id = Column(Integer, primary_key=True, index=True) + key = Column(String(50), unique=True, index=True, nullable=False) + value = Column(JSON, nullable=False) + is_active = Column(Boolean, default=True) + description = Column(String, nullable=True) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/models/vehicle_catalog.py b/backend/app/models/vehicle_catalog.py deleted file mode 100755 index 587e9a1..0000000 --- a/backend/app/models/vehicle_catalog.py +++ /dev/null @@ -1,54 +0,0 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Float, JSON, Date -from sqlalchemy.orm import relationship -from app.db.base import Base - -# 1. Kategória (Autó, Motor, Kisteher...) -class VehicleCategory(Base): - __tablename__ = "vehicle_categories" - __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) - name_key = Column(String, nullable=False) # i18n kulcs: 'CAR', 'MOTORCYCLE' - -# 2. Márka (Audi, Honda, BMW...) -class VehicleMake(Base): - __tablename__ = "vehicle_makes" - __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) - name = Column(String, unique=True, nullable=False) - logo_url = Column(String, nullable=True) - -# 3. Modell és Generáció (pl. Audi A3 -> A3 8V) -class VehicleModel(Base): - __tablename__ = "vehicle_models" - __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) - make_id = Column(Integer, ForeignKey("data.vehicle_makes.id")) - category_id = Column(Integer, ForeignKey("data.vehicle_categories.id")) - name = Column(String, nullable=False) - generation_name = Column(String, nullable=True) # pl: "8V Facelift" - production_start_year = Column(Integer, nullable=True) - production_end_year = Column(Integer, nullable=True) - -# 4. Motor és Hajtáslánc (Technikai specifikációk) -class VehicleEngine(Base): - __tablename__ = "vehicle_engines" - __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) - model_id = Column(Integer, ForeignKey("data.vehicle_models.id")) - - engine_code = Column(String, nullable=True) - fuel_type = Column(String, nullable=False) # 'Petrol', 'Diesel', 'Hybrid', 'EV' - displacement_ccm = Column(Integer, nullable=True) - power_kw = Column(Integer, nullable=True) - torque_nm = Column(Integer, nullable=True) - transmission_type = Column(String, nullable=True) # 'Manual', 'Automatic' - gears_count = Column(Integer, nullable=True) - drive_type = Column(String, nullable=True) # 'FWD', 'RWD', 'AWD' - -# 5. Opciók Katalógusa (Gyári extrák listája) -class VehicleOptionCatalog(Base): - __tablename__ = "vehicle_options_catalog" - __table_args__ = {"schema": "data"} - id = Column(Integer, primary_key=True) - category = Column(String) # 'Security', 'Comfort', 'Multimedia' - name_key = Column(String) # 'MATRIX_LED' \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/asset.cpython-312.pyc b/backend/app/schemas/__pycache__/asset.cpython-312.pyc deleted file mode 100644 index a65dd0f8dcb3dc3eb22318feed3c4f405332e48e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2419 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 delta 1266 zcmZ`&O-~a+7@p~Vbi3Pbzk%{02pCvJi!p?tNJ!Keqqcf5Asiq;E2*|!oUOt7g$bDG z4Kf!G#*;Voao)Ey)QxPi&&)nM`@GM*&+NONk8S4rP)H}@ zN*hlLv&xp)LBDPd4FxI5k(^ZYm$X@JYp#7Q;3MZrPTe3mf1bXQ;0r=#b!0Wc23lAH zSzWM33mZf>DA-U78$#9;Y`BFrk&Os8+F<*zTo~z?pyMrzBh`eJ zUMRZq*}_uAqOP18!4x*KjR3}ZpPJ%t1NYWI*i|a_&3yS7gR~g*0;6{$D+`XZ zYO~z@l3l)DU?p+B_IH47P?{gJQ!w+mng9+UEp7*ZztKk{yKDI&DG$?suW6jhN0mff=3sl!q6}v zY$W=|LjP{a)h0`J?shRh#m)(F3Jz`w_#shBcS!mJN$-$@pGm5&DztmES|?(tn?B0( o=HT(}%|e}+g6O9-@ob__#86jdItFtw)PoXDJh||Thyi2!3#YW=#Q*>R diff --git a/backend/app/schemas/__pycache__/organization.cpython-312.pyc b/backend/app/schemas/__pycache__/organization.cpython-312.pyc index e564d1e1ac377daf247664b8239b974bf6741307..f057bd14cb42994d748e88eabd3b172361b95ad0 100644 GIT binary patch delta 631 zcmX@kze&$d#-N+}!#Mm-flc}+ui6NCag(rn~4fARykOBsVD6Ul2 zRJIg88z37j%by~EB+Ct!6$Htq2u1Ov@}{y%0@*+v!XPoQtO%SX3RTC4q)rSf#t#&$ zhiMjv3JM@8lz@r}B8f>t#e|T=q@ZHLNMh1ZF%cv&8IV{yLzHN$SgJV0(OfCAAi+u@ zO}WWs%vb7fF=dpNDg#9v6^g4;i^@`~QcDyPa}o;dFUTSXYErFEOw8YY!lKAA(qN3Eih6C##q0 zO%cf%Di?GDuZRRScyuIRW(m3}qqsozf@$m(nYarqA`^f@ahor*S}>~Vf%Jk1kl%}W zfkZ3A4N-~9ED|?(_*=Y-M8NWs4cW~Z^(JSs%QBiwZf94O7H3qS;Q56CNPRJ4VicI6 L`IP}kfmH(l@okh9 delta 394 zcmZ1_c$}Z_G%qg~0}#CNZqJ;`vXM`UiP2-SCR1a53QrVQDt9WYBv3MyErr(xM6&`} zd~g;&hy_%~gQQLXD#nW>CI}VdLlP5$it!_f2}8vMfMUKdi$$P*JB7nH$74nP6{yhIM-Em2MqluIJRkZ%SEUmPHoumZAZ#0U(ZbfZM(l1oz6v&)}( z$pBMbUDI7tU9YOT{#GpJ2t4i=SN$(dLcYOC|1o;Z`tN|bK|JCq0SOgb2~}GSHCqdH zTUSu721aPwrsTCC6I!;V5S6@6JpDuB8C1EWrMm2_l$s#T(Cnr(C#4ohv)iP3Db0a2 zzfD7-S*R2rp}LALvOjS-eLMDOAgrm-^@DQ>6N9rp4Ls+H8~C1^#7yK~jiXt=_EW!_ zR1{&oQ&0Rjas%LXSU{_(Z7J;;O4jQzzdz9%Nci zW^I${QfAm$kmY*oOexFQd76V=mYikn0?oiIhqIJ_SpAC*5{5}P5dkLHBkVbH|mr%V7E}uoELdUYj8@Z3eKq%h2=;Ejw5o86UJU6 zpuiU#=T{9kK-G@Jbg)4TI*uDfapIznI7ka1Wf}aW@r1D=P6u$R&%|-S260{ja+|CJ zCI5N)Ik#SyFRxyLBkC z;e|IA-(S&|u66Xo_zh?J(g*l$|HQ&u?IRt1qC0kA;m!8QL;V0l|4O|(fJBq=S^qnb z{{u+fwrCb0F(g2$odXE99T2Jj;g&3e>r_Guo^?mTA3{(RA&xaKN3~hq%YUffNyVf; z+5>WZX2<%TM%dNkU~gfMy@L=YMNz;oISUZ;1OT_{F6UQc=1mK0#%IYT&uzXYau5{N zIIO!-Qxtkk8b-Oqecy>1;S6Og55@`QiZB^X8Z4@+J!5geWWcwV3^k|#e^Rcg^UCGk zNpVsC@znl#Jn65?{Z#5NfU)m^YudavuU;W6pY*E?)QbN8-zf9SIZ`pED|%}HlfaKK zk59lET0_sD4E?B)$mEccBJlO&xa>sCYOPYO0jKhVz?CA{Ba|OK`Y$|+p6k zPwc(uDNw*)0kS}zm}H-;EM~i<;TvPiV=E)SA77byP&)LF(y<4nW1UiE(d_0+?Vmr$ zPyBOu{QkjrJHu!17tVGEhZZfFEY%9PV&^>$QtTV>ZCKFLZ`4-Y;C-a(rco4Wlt5NJfz$tZW~W2_)zyhMtfxbJ6Vdbx{i%Q<<&d-cz5zB zRMzjVys&({qaWQ_k-s)Z+ZQ{=!LBj1rW!`+k9>{5aEm{|VO=ApBok{oOdk&rb6@)x z9b-~1*I>DIYvg8ejR1Sk`fTLW;uB;yw;g`0!!)%W&91_``Sc@o{S1iquHw{yYOTs@ zt=VubQ>$04?OO%mqM zK(XPK{4V5#=0{12+!jg_J_#vaaiRbhEe)UsWB0KeAq;hfe*ISJDcrfYr+x`vwtLD`Y-nAmA-!VuJf+ysc5KSh7Dj=aZ%dZ8 zFbcFVdas2s(>t-LR7{b>EID7t_Gy|Ogg>Tzg6t0PM){V%(nVVik})rPi_JZ<0om=Q z)2JKCNqG4Wfqi6k`%RGilcSsFt=q79KS5Lz*V1>||t$Nff1xI}=b?FQ;ABE9}Rh60b{+%$lYs z%D>1|hfIA%4tz!SZ&kc9`~OW~kVp6* DePq9| diff --git a/backend/app/schemas/asset.py b/backend/app/schemas/asset.py index fd661d7..c5ab1e5 100644 --- a/backend/app/schemas/asset.py +++ b/backend/app/schemas/asset.py @@ -1,43 +1,54 @@ -from pydantic import BaseModel, Field -from typing import Optional +from pydantic import BaseModel, ConfigDict, Field +from typing import Optional, Dict, Any, List from uuid import UUID from datetime import datetime -class AssetCreate(BaseModel): - # Alapadatok - make: str = Field(..., example="Ford") - model: str = Field(..., example="Mondeo") - vin: str = Field(..., min_length=17, max_length=17, description="Alvázszám") - license_plate: Optional[str] = Field(None, max_length=20, example="RRR-555") - - # Nemzetközi és Admin szempontok - vehicle_class: str = Field("land", description="land, sea, air - Admin által bővíthető") - fuel_type: str = Field(..., example="Diesel", description="Admin által definiált üzemanyag típusok") - - # Technikai adatok - engine_description: Optional[str] = Field(None, example="2.0 TDCI") - year_of_manufacture: int = Field(..., ge=1900, le=2100) - - # Kezdő állapot - current_reading: int = Field(..., ge=0, description="Kezdő km/üzemóra állás") - reading_unit: str = Field("km", description="km, miles, hours - Nemzetközi beállítás") - - # Felhasználói adatok - name: Optional[str] = Field(None, description="Egyedi elnevezés") +# --- KATALÓGUS SÉMÁK (Gyári adatok) --- +class AssetCatalogBase(BaseModel): + make: str + model: str + generation: Optional[str] = None + year_from: Optional[int] = None + year_to: Optional[int] = None + vehicle_class: Optional[str] = None + fuel_type: Optional[str] = None + engine_code: Optional[str] = None -class AssetResponse(BaseModel): - id: UUID - catalog_id: Optional[int] - vin: str - license_plate: Optional[str] = None # JAVÍTVA: Lehet None a válaszban +class AssetCatalogResponse(AssetCatalogBase): + id: int + factory_data: Optional[Dict[str, Any]] = None # A robot által gyűjtött adatok + + model_config = ConfigDict(from_attributes=True) + +# --- JÁRMŰ SÉMÁK (Asset) --- +class AssetBase(BaseModel): + vin: str = Field(..., min_length=17, max_length=17) + license_plate: str name: Optional[str] = None - fuel_type: str - vehicle_class: str - is_verified: bool - year_of_manufacture: int - system_mileage: int - quality_index: float - created_at: datetime + year_of_manufacture: Optional[int] = None - class Config: - from_attributes = True \ No newline at end of file +class AssetCreate(AssetBase): + # A létrehozáshoz kellenek a katalógus infók is + make: str + model: str + vehicle_class: Optional[str] = "land" + fuel_type: Optional[str] = None + current_reading: Optional[int] = 0 + +class AssetResponse(AssetBase): + id: UUID + catalog_id: int + is_verified: bool + status: str + + model_config = ConfigDict(from_attributes=True) + +# --- DIGITÁLIS IKER (Full Profile) --- +# Ez a séma felel a 9 pontos költség és a mélységi szerviz adatok átadásáért +class AssetFullProfile(BaseModel): + identity: Dict[str, Any] + telemetry: Dict[str, Any] + financial_summary: Dict[str, Any] + service_history: List[Dict[str, Any]] + + model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/backend/app/schemas/asset_cost.py b/backend/app/schemas/asset_cost.py new file mode 100644 index 0000000..815aba9 --- /dev/null +++ b/backend/app/schemas/asset_cost.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any +from datetime import datetime +from decimal import Decimal +from uuid import UUID + +class AssetCostBase(BaseModel): + """Alap költség adatok (Frontendről érkező bevitel).""" + cost_type: str = Field(..., description="fuel, service, fine, insurance, toll, etc.") + amount_local: Decimal = Field(..., description="A fizetett bruttó összeg helyi devizában") + currency_local: str = Field("HUF", min_length=3, max_length=3) + date: datetime = Field(default_factory=datetime.now) + mileage_at_cost: Optional[int] = Field(None, description="Kilométeróra állása a költség rögzítésekor") + description: Optional[str] = None + net_amount_local: Optional[Decimal] = None + vat_rate: Optional[Decimal] = Field(27.0, description="ÁFA kulcs (pl. 27.0)") + data: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Extra adatok (pl. helyszín, számlaszám)") + +class AssetCostCreate(AssetCostBase): + """Költség rögzítésekor használt séma.""" + asset_id: UUID + organization_id: int + +class AssetCostResponse(AssetCostBase): + """Visszatérő adat modell a frontend felé.""" + id: UUID + asset_id: UUID + organization_id: int + driver_id: Optional[int] + amount_eur: Decimal + exchange_rate_used: Decimal + created_at: Optional[datetime] = None + + class Config: + from_attributes = True \ No newline at end of file diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 37689f0..ac72049 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, EmailStr, Field -from typing import Optional, Dict +from typing import Optional, Dict, Any from datetime import date # --- STEP 1: LITE REGISTRATION --- @@ -9,6 +9,8 @@ class UserLiteRegister(BaseModel): first_name: str last_name: str region_code: str = "HU" + lang: str = Field("hu", description="Választott nyelv kódja") + timezone: str = Field("Europe/Budapest", description="Felhasználó időzónája") class UserLogin(BaseModel): email: EmailStr @@ -30,15 +32,15 @@ class UserKYCComplete(BaseModel): birth_date: date mothers_last_name: str mothers_first_name: str - # Hibrid Címmezők address_zip: str address_city: str address_street_name: str address_street_type: str address_house_number: str - address_hrsz: Optional[str] = None # Helyrajzi szám + address_hrsz: Optional[str] = None identity_docs: Dict[str, DocumentDetail] ice_contact: ICEContact + preferred_currency: Optional[str] = Field("HUF", max_length=3) # --- COMMON & SECURITY --- class PasswordResetRequest(BaseModel): @@ -53,4 +55,13 @@ class PasswordResetConfirm(BaseModel): class Token(BaseModel): access_token: str token_type: str - is_active: bool \ No newline at end of file + is_active: bool + +class TokenPayload(BaseModel): + """JWT Token payload struktúrája validációhoz.""" + sub: Optional[str] = None + role: Optional[str] = None + rank: Optional[int] = 0 + scope_level: Optional[str] = None + scope_id: Optional[str] = None + region: Optional[str] = None \ No newline at end of file diff --git a/backend/app/schemas/auth_old.py b/backend/app/schemas/auth_old.py deleted file mode 100755 index 9f541c4..0000000 --- a/backend/app/schemas/auth_old.py +++ /dev/null @@ -1,46 +0,0 @@ -from pydantic import BaseModel, EmailStr, Field, field_validator -from typing import Optional, List -from datetime import date - -class UserRegister(BaseModel): - # --- AUTH --- - email: EmailStr = Field(..., example="teszt.user@profibot.hu") - password: Optional[str] = Field(None, min_length=8, description="Social login esetén üres maradhat") - - # --- IDENTITY (KYC Step 2) --- - last_name: str = Field(..., min_length=2) - first_name: str = Field(..., min_length=2) - mothers_name: str = Field(..., description="Anyja születési neve") - birth_place: Optional[str] = None - birth_date: Optional[date] = None - - # --- OKMÁNYOK (Banki szint) --- - id_card_number: Optional[str] = None - id_card_expiry: Optional[date] = None - - driver_license_number: Optional[str] = None - driver_license_expiry: Optional[date] = None - driver_license_categories: List[str] = Field(default_factory=list, example=["B", "A"]) - - # --- SPECIÁLIS ENGEDÉLYEK --- - boat_license_number: Optional[str] = None - pilot_license_number: Optional[str] = None - - # --- SYSTEM --- - region_code: str = Field(default="HU") - invite_token: Optional[str] = None - social_provider: Optional[str] = None - social_id: Optional[str] = None - - @field_validator('region_code') - @classmethod - def validate_region(cls, v: str) -> str: - return v.upper() if v else "HU" - -class Token(BaseModel): - access_token: str - token_type: str - -class UserLogin(BaseModel): - email: EmailStr - password: str \ No newline at end of file diff --git a/backend/app/schemas/organization.py b/backend/app/schemas/organization.py index a32ab77..8ded139 100644 --- a/backend/app/schemas/organization.py +++ b/backend/app/schemas/organization.py @@ -15,6 +15,8 @@ class CorpOnboardIn(BaseModel): tax_number: str country_code: str = "HU" + language: str = Field("hu", description="A szervezet alapértelmezett nyelve") + default_currency: str = Field("HUF", description="A szervezet alapértelmezett pénzneme") reg_number: Optional[str] = None # Atomizált Címkezelés diff --git a/backend/app/scripts/seed_system_params.py b/backend/app/scripts/seed_system_params.py new file mode 100644 index 0000000..58d7e8e --- /dev/null +++ b/backend/app/scripts/seed_system_params.py @@ -0,0 +1,43 @@ +import asyncio +import json +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from app.models.system_config import SystemParameter +from app.core.config import settings + +async def seed_system(): + engine = create_async_engine(settings.DATABASE_URL) + async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + async with async_session() as session: + params = [ + { + "key": "fuel_types", + "value": ["Benzin (95)", "Benzin (100)", "Dízel", "Prémium Dízel", "LPG", "Elektromos", "Hibrid"], + "description": "Rendszerben használható üzemanyag típusok" + }, + { + "key": "currencies", + "value": ["HUF", "EUR", "USD", "GBP"], + "description": "Támogatott pénznemek" + }, + { + "key": "expense_categories", + "value": ["Üzemanyag", "Szerviz", "Biztosítás", "Autópálya matrica", "Parkolás", "Adó", "Egyéb"], + "description": "Költség kategóriák" + } + ] + + for p in params: + # Megnézzük, létezik-e már + from sqlalchemy import select + result = await session.execute(select(SystemParameter).where(SystemParameter.key == p["key"])) + if not result.scalar_one_or_none(): + new_param = SystemParameter(**p) + session.add(new_param) + + await session.commit() + print("✅ Rendszer paraméterek sikeresen feltöltve!") + +if __name__ == "__main__": + asyncio.run(seed_system()) \ No newline at end of file diff --git a/backend/app/seed_system.py b/backend/app/seed_system.py index 75a3b05..6ff8e3d 100755 --- a/backend/app/seed_system.py +++ b/backend/app/seed_system.py @@ -1,58 +1,97 @@ import asyncio -from datetime import datetime +import logging +import uuid from sqlalchemy import select from app.db.session import SessionLocal -from app.models.legal import LegalDocument -from app.models.email_template import EmailTemplate, EmailType -from app.models.email_provider import EmailProviderConfig +from app.models import ( + User, Person, UserRole, SystemParameter, + PointRule, LevelConfig, SubscriptionTier, UserStats +) +from app.core.security import get_password_hash +from app.core.config import settings + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) async def seed_data(): async with SessionLocal() as db: - # 1. Jogi dokumentumok (HU) - legal_docs = [ - LegalDocument( - title="Általános Szerződési Feltételek", - content="Ide jön az ÁSZF szövege... Kérjük görgessen az aljáig.", - version="v1.0", - region_code="HU", - language="hu" - ), - LegalDocument( - title="Adatkezelési Tájékoztató (GDPR)", - content="Ide jön a GDPR szövege... Kérjük görgessen az aljáig.", - version="v1.0", - region_code="HU", - language="hu" + logger.info("🚀 Alapadatok feltöltése biztonságos módban...") + + admin_email = settings.INITIAL_ADMIN_EMAIL + admin_password = settings.INITIAL_ADMIN_PASSWORD + + if not admin_email or not admin_password: + logger.error("❌ HIBA: INITIAL_ADMIN_EMAIL vagy PASSWORD nincs beállítva!") + return + + stmt = select(User).where(User.email == admin_email) + admin_exists = (await db.execute(stmt)).scalar_one_or_none() + + if not admin_exists: + new_person = Person( + first_name="Rendszer", + last_name="Adminisztrátor", + id_uuid=uuid.uuid4() ) + db.add(new_person) + await db.flush() + + new_admin = User( + email=admin_email, + hashed_password=get_password_hash(admin_password), + role=UserRole.admin, + is_active=True, + # JAVÍTÁS: is_verified eltávolítva, mert nincs ilyen mező a modellben + person_id=new_person.id + ) + db.add(new_admin) + await db.flush() + + db.add(UserStats(user_id=new_admin.id, total_xp=0, current_level=1)) + logger.info(f"✅ Admin létrehozva: {admin_email}") + + # --- 1. Értékelési szempontok (Admin Motor) --- + criteria_key = "ASSET_REVIEW_CRITERIA" + stmt_crit = select(SystemParameter).where(SystemParameter.key == criteria_key) + if not (await db.execute(stmt_crit)).scalar_one_or_none(): + db.add(SystemParameter( + key=criteria_key, + value=["Kényelem", "Fogyasztás", "Megbízhatóság", "Vezetési élmény", "Szervizigény"], + description="Járműértékelési szempontok" + )) + + # --- 2. Gamification Pontszabályok --- + rules = [ + ("ASSET_REGISTER", 100, "Új jármű felvétele"), + ("ASSET_REVIEW", 75, "Jármű értékelése"), + ("COST_RECORD", 50, "Költség/Tankolás rögzítése") ] - - # 2. Email Sablon (Regisztráció) - reg_template = EmailTemplate( - type=EmailType.REGISTRATION, - subject="Üdvözöljük a Service Finderben!", - body_html=""" -

Kedves {{ name }}!

-

Köszönjük a regisztrációt! Az aktiváláshoz kattints ide:

- Fiók aktiválása -

A link 24 óráig érvényes.

- """ - ) + for key, pts, desc in rules: + stmt_rule = select(PointRule).where(PointRule.action_key == key) + if not (await db.execute(stmt_rule)).scalar_one_or_none(): + db.add(PointRule(action_key=key, points=pts, description=desc)) - # 3. Email Szolgáltató (SendGrid) - sendgrid_provider = EmailProviderConfig( - name="SendGrid_Primary", - provider_type="SENDGRID", - priority=1, - settings={"api_key": "YOUR_SENDGRID_KEY_HERE"}, # Ezt majd az adminon írjuk át - max_fail_threshold=3 - ) + # --- 3. Gamification Szintek --- + stmt_level = select(LevelConfig) + if not (await db.execute(stmt_level)).first(): + db.add_all([ + LevelConfig(level_number=1, min_points=0, rank_name="Kezdő Sofőr"), + LevelConfig(level_number=2, min_points=500, rank_name="Tapasztalt Vezető"), + LevelConfig(level_number=3, min_points=2000, rank_name="Flotta Mester") + ]) + + # --- 4. Előfizetési csomagok (MVP korlátok) --- + stmt_tier = select(SubscriptionTier) + if not (await db.execute(stmt_tier)).first(): + db.add_all([ + SubscriptionTier(name="Ingyenes", rules={"max_assets": 1, "reports": False}), + SubscriptionTier(name="Prémium", rules={"max_assets": 5, "reports": True}), + SubscriptionTier(name="Flotta", rules={"max_assets": 100, "reports": True}) + ]) - db.add_all(legal_docs) - db.add(reg_template) - db.add(sendgrid_provider) await db.commit() - print("🌱 Alapadatok sikeresen feltöltve!") + logger.info("✨ A rendszer alapadatai és a Gamification motor készen áll!") if __name__ == "__main__": - asyncio.run(seed_data()) + asyncio.run(seed_data()) \ No newline at end of file diff --git a/backend/app/seed_test_scenario.py b/backend/app/seed_test_scenario.py new file mode 100644 index 0000000..ab6db8e --- /dev/null +++ b/backend/app/seed_test_scenario.py @@ -0,0 +1,107 @@ +import asyncio +import uuid +from datetime import datetime, timedelta +from sqlalchemy import select +from app.db.session import SessionLocal +from app.models import ( + User, Organization, OrganizationMember, Asset, AssetCatalog, + AssetTelemetry, AssetFinancials, AssetCost, AssetEvent +) +from app.models.organization import OrgType + +async def seed_test_scenario(): + async with SessionLocal() as db: + print("🚀 Teszt ökoszisztéma felépítése a meglévő modellek alapján...") + + # 1. Admin lekérése + admin = (await db.execute(select(User))).scalars().first() + if not admin: + print("❌ Hiba: Nincs admin az adatbázisban!") + return + + # 2. SZERVEZETEK (A te OrgType enumod alapján) + # Privát flotta + private_org = Organization( + name="Kincses Privát", + full_name="Kincses Magánflotta és Garázs", + org_type=OrgType.individual, + owner_id=admin.id + ) + # Céges flotta (OrgType.business-t használunk!) + company_org = Organization( + name="ProfiBot Fleet", + full_name="ProfiBot Software Solutions Kft.", + org_type=OrgType.business, + owner_id=admin.id + ) + # Szolgáltatók + service_org = Organization( + name="Mester Szerviz", + full_name="Mester Autójavító és Vizsgabázis Kft.", + org_type=OrgType.service, + owner_id=admin.id + ) + gas_station = Organization( + name="MOL Digit", + full_name="MOL Digitális Töltőállomás 001", + org_type=OrgType.service_provider, # OrgType.service_provider-t használunk! + owner_id=admin.id + ) + + db.add_all([private_org, company_org, service_org, gas_station]) + await db.flush() + + # Tagságok rögzítése + db.add(OrganizationMember(user_id=admin.id, organization_id=private_org.id, role="owner")) + db.add(OrganizationMember(user_id=admin.id, organization_id=company_org.id, role="owner")) + + # 3. RÉTESZLETES JÁRMŰ ADAT (Tesla Model 3) + catalog = AssetCatalog( + make="Tesla", model="Model 3", generation="Long Range", + year_from=2021, fuel_type="Electric", + factory_data={ + "battery": "75 kWh", "power": "366 kW", "torque": "493 Nm", + "tire_size": "235/45 R18", "oil_type": "None (EV)" + } + ) + db.add(catalog) + await db.flush() + + vehicle = Asset( + vin="5YJ3E1EB8LF000000", license_plate="TES-777-EV", + name="Főnök Teslája", year_of_manufacture=2021, + catalog_id=catalog.id, status="active" + ) + db.add(vehicle) + await db.flush() + + # Telemetria és Pénzügyi modulok + db.add(AssetTelemetry(asset_id=vehicle.id, current_mileage=45200, vqi_score=100.0, dbs_score=100.0)) + db.add(AssetFinancials(asset_id=vehicle.id, acquisition_price=18500000)) + + # 4. KÖLTSÉGEK (9 kategória szimulálása) + costs_data = [ + ("FUEL", 15000, "Szupertöltés MOL", gas_station.id), + ("MAINTENANCE", 120000, "Éves szerviz + fékfolyadék", service_org.id), + ("TIRES", 240000, "Michelin Pilot Sport szett", None), + ("INSURANCE", 45000, "Allianz Casco", None), + ("TAX", 0, "Zöld rendszám kedvezmény", None), + ("TOLL", 5500, "Pest megyei e-matrica", None), + ("CLEANING", 8500, "Nano bevonat + Mosás", None), + ("PARKING", 2400, "Airport Parking", None), + ("FINE", 0, "Nincs aktív bírság", None) + ] + + for c_type, amount, desc, vendor_id in costs_data: + db.add(AssetCost( + asset_id=vehicle.id, organization_id=company_org.id, + cost_type=c_type, amount=amount, currency="HUF", + data={"description": desc, "vendor_id": vendor_id}, + date=datetime.now() - timedelta(days=2) + )) + + await db.commit() + print("✅ Siker! Flották, Tesla és a 9 költségtípus rögzítve.") + +if __name__ == "__main__": + asyncio.run(seed_test_scenario()) \ No newline at end of file diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc index 8de3f8ac89206629be9f9c9fb73fdf8ffaccd287..091f5b53fffa168b881f7254ae1e2d837ad674c1 100644 GIT binary patch delta 3754 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 diff --git a/backend/app/services/__pycache__/cost_service.cpython-312.pyc b/backend/app/services/__pycache__/cost_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc10c9de637b4aaff848ec3d9415bdd5658cb11b GIT binary patch literal 5417 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 diff --git a/backend/app/services/__pycache__/gamification_service.cpython-312.pyc b/backend/app/services/__pycache__/gamification_service.cpython-312.pyc index 4f49e9ba7e973fa264510f966853ec68e41ea9cf..04264e8f20dc874881fd56b4463ace769993deb7 100644 GIT binary patch literal 2391 zcmZ`5ZD&$7GYcy>oUi zCf7|{s+4FWu^>%KDUHw{N-PM4lD`s!K>yq&kvq1PwzP%%?`i^V{neShUGI=M$IN?g z=6%hZ_uk%*)zwu1kMGZ4Q{VOi@H-*g!8pkJn;4k|3Q(vxNKi3~BDg2+NzgIc#_2ed zU}J28i*Yv1#Jw@E2b=^7dj%-mf~TY@#!oO&?_)wlDN8u2PiXQW)O9tfVVcun9Lh%2 zWBJbL5DglVpiU+a^jVFUA z3%Ltb3U`HBa71F>32&6Q>P|`t^@1u(2KHbOqA^v5mPdKt@}{u^qN-TF@subfl4;Gb zs`aF-N^zIu5R~+!CbN!80(d+xU2c;yJtl23ci9Ce!760AacU&wa+aa#$zZFp zMkPhxcU`3gM}u&rWebEUHLK`Jq`e}Per>Z$cafj+FIsu zF_U&YdKOWva}z#=A3cw?#$I%q!C5iM+g7%@y35ey9V>#n_ac83K7%8IBUJWh)0e7z z2gPUdUje1+Yue=R`WKv+ccW#$WRq*R4BdQrKDRJGHwgBF8`O07elYF(j=n@ih4g7G zSru`{+0zki>4z~GU%I33jSOaRX=5~nsfet@(a{LOj;4)dLNcUCKZ1&CM8=kG#f_y~ zdgn7Wrx2;7$E1v+EFy1Z@kY|X#W|jOR;MYuaah%I8X>3|VjN<-mRGiw>QSa7=fwp+ zB&nL^cgmX(h@`l>f%OlavB#gUKUVNoriS>|At$vlh)CBu6v^7Llaq;rYD9gA^nsECBm)phn35}uZX^tg)$Mlb)8Hj15`Ka@Zt6tZeWb|K zXqS{q*%P-x(iHWsp^8o^I=h{z2`iLBNqnx0l5D7Bsxgt-xk*#)e4Yz`YJh$o2ly}W z@5f+60HLin)zx4mAKaY_?*4Wn-*zC^c3|;juI<=r@c0x{sBf6fe4d%#xmw?ruW8GN zdUBzj)zFbC_P1d8=kWIVp+#!BCL24Kg%{SsLsNYPAuw~|>WLfO(*s2g`1~{6Rc_iW zgqsTC?S*Z-3Uv(|J~mXf0a%~E7y`b)OyF8zE}G+8uLu4J*UxSFruoa}+pW2Vj(gp? zZ3kDwhZf(-g?p!b1%LQPI$z(Ot8ZWNw-{YWMtCe1ihM#;5u&9OX}rGA96apQHMEnET%29hi8qkLv4X9&~#N-b>;26#f|U zqNqs;D2i55$JjZw*LZpeggh~m?*X73I118^7Mb}Cn}Br literal 1721 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 R^&#D`$XMZd$aqbo{{vxq1pxp6 diff --git a/backend/app/services/__pycache__/media_service.cpython-312.pyc b/backend/app/services/__pycache__/media_service.cpython-312.pyc deleted file mode 100644 index b4d73ea13902da1b69ed43d6181e490cd033cf83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2674 zcmZ`*O>7&-6`uX&uYPPQ`k^d!D?7HBigb$9P3i_k97U3qI7}M3MM9)lY`8;u#U+>B zUCN@cG>pOr88uMqBpO9v=~SogLAm9Sg94&Jfm}o@XqbtL80g72+6vIZz4Xm4DT!`9 z0B7FcyqP!eeQ)_?PfrlRcx8Q6+ZGY}CuKT=Zxj1PAl8wJR7OX6CdDw6W_32trMNtw z;$5E8g_HntUKjJelrQg3`5D9_EG!=HI6Pc&@E+rFsSu0ikSg3js(6oW%fe+Y=6gt) z7(;~lypqK+mhhJ^%qMkz)` zDHd}oa|7LLD^oo1EEY1X%H7}(q$2jIJobYofP+||`VrVce;FPwduSOdi!mPwzl~K* zS-|F!md5FJ81502=HRzK2-7-p7zZsOQ*eNy-WE_9VoZL5Xnd+-D}>A8vPE}E(vyj^ zO0nqvEYG!dHct&wE77AjNp$G~;MAhmxQ-r#daI{y{=69( zs&Q*S+X;s(xB|)R;B(uVF>D)Iv(6L=Q zu4^9$@4~A5N(iA9@aJ%<(4iMlOf1cVaOVB_St(~2rm7X{n|6IOCrxII+>}(rW_n7> z*EcOgrF0Ce3QO1o6CH$#WNRx}#g+utx%y_pq_6vs2Jw(mS@^+7zUC3qUtXg{m(dNy zj`2iTQuGogj7pe1VJwTjg#^Rw1%gE6w0D$j8?uVCCdSr^)SZI=wdjMb*6T1`N8gL6 z=Xmw`8nZU}`*&8Ez3{PTFwMUH>V+D=cJb!5jbj_X-i$W-rgr>OWD1KYtzg>yKIDKi?X@D8`=d$#2p=r^+n zeF&#jgo5_HF++jcsfw>p9&bXq{g=lE1MHwh2L+?sGi@FRf_zunW>^M5_~9shxc zfFnA7T4fIJ1YU*p)&!4kUF3l`XC1Uiv+&e#c(uDOgCOCl4^_y$i~zT)=mb~b?v>y( zJ@~pRI|LrwTd08p4$O$^347&h%DKjULO*83^Q^=gCgzba2x%ga1bGWKOC;UhB zG26))gFXN(R>If9WyV{tGFQkvb&NZ7Uy#N&(^lL%_)Sv7&Tz|G`$r_E7 zvz4q#8zk|mIBlpHKyQOWHA#h_w0ixGYON<7F_XbAR-&}jMR^AjhPquvM2QNPAMg7Cg!I7^AR^)!FIMN$92zrXjP{*lLsg`mU7|B$?y+!>qu8?$ZgjK1|%_|jfv=szt1 zluxXa@J7EoGu?;&{1QxGj9*B=Qtt~)pXchs!_()u`Z)pUWGoEjA{UfAmSqx@<-DPm zpf$=QEX%(wDSBHIwrr^BbROGR4ArEaz@#AaN-31q0VbY~aF%VFw85FQo0>nR6MUtR z+j!l4o3yF96aB|w$uQkOP2t!hmJk4of4lMHuLIFWAX Person {existing_person.id}") @@ -118,7 +126,7 @@ class AuthService: parcel_id=kyc_in.address_hrsz ) - # 4. Person adatok frissítése (mindig a legfrissebbet tároljuk) + # 4. Person adatok frissítése active_person.mothers_last_name = kyc_in.mothers_last_name active_person.mothers_first_name = kyc_in.mothers_first_name active_person.birth_place = kyc_in.birth_place @@ -129,7 +137,7 @@ class AuthService: active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact) active_person.is_active = True - # 5. Új, izolált INDIVIDUAL szervezet (4.2.3) + # 5. Új, izolált INDIVIDUAL szervezet (4.2.3) i18n beállításokkal new_org = Organization( full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta", name=f"{active_person.last_name} Flotta", @@ -137,7 +145,11 @@ class AuthService: owner_id=user.id, is_transferable=False, is_active=True, - status="verified" + status="verified", + # Megörökölt adminisztrációs adatok + language=user.preferred_language, + default_currency=user.preferred_currency, + country_code=user.region_code ) db.add(new_org) await db.flush() @@ -150,8 +162,13 @@ class AuthService: permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True} )) - # 7. Wallet & Stats (Friss kezdés 0 ponttal) - db.add(Wallet(user_id=user.id, coin_balance=0, credit_balance=0)) + # 7. Wallet & Stats + db.add(Wallet( + user_id=user.id, + coin_balance=0, + credit_balance=0, + currency=user.preferred_currency + )) db.add(UserStats(user_id=user.id, total_xp=0, current_level=1)) # 8. Aktiválás @@ -197,7 +214,6 @@ class AuthService: @staticmethod async def initiate_password_reset(db: AsyncSession, email: str): - # Csak aktív (nem törölt) felhasználónak engedünk jelszót resetelni stmt = select(User).where(and_(User.email == email, User.is_deleted == False)) user = (await db.execute(stmt)).scalar_one_or_none() @@ -211,11 +227,13 @@ class AuthService: expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_hours)) )) + # --- EMAIL KÜLDÉSE A FELHASZNÁLÓ SAJÁT NYELVÉN --- reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}" await email_manager.send_email( recipient=email, - template_key="password_reset", - variables={"link": reset_link} + template_key="pwd_reset", # hu.json: email.pwd_reset_subject stb. + variables={"link": reset_link}, + lang=user.preferred_language # Adatbázisból kinyert nyelv ) await db.commit() return "success" diff --git a/backend/app/services/cost_service.py b/backend/app/services/cost_service.py new file mode 100644 index 0000000..7bc1d64 --- /dev/null +++ b/backend/app/services/cost_service.py @@ -0,0 +1,97 @@ +import logging +from decimal import Decimal +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, desc +from app.models.asset import AssetCost, AssetTelemetry, ExchangeRate +from app.models.gamification import UserStats +from app.models.system_config import SystemParameter +from app.schemas.asset_cost import AssetCostCreate +from datetime import datetime + +logger = logging.getLogger(__name__) + +class CostService: + @staticmethod + async def get_param(db: AsyncSession, key: str, default: any) -> any: + """Rendszerparaméter lekérése (pl. XP szorzó).""" + stmt = select(SystemParameter).where(SystemParameter.key == key) + res = await db.execute(stmt) + param = res.scalar_one_or_none() + return param.value if param else default + + async def record_cost(self, db: AsyncSession, cost_in: AssetCostCreate, user_id: int): + """ + Költség rögzítése: EUR konverzió + Telemetria + XP. + """ + try: + # 1. Árfolyam lekérése (EUR alapú pivot) + # Megkeressük a legfrissebb rögzített árfolyamot a megadott devizához + rate_stmt = select(ExchangeRate).where( + ExchangeRate.target_currency == cost_in.currency_local + ).order_by(desc(ExchangeRate.updated_at)).limit(1) + + rate_res = await db.execute(rate_stmt) + rate_obj = rate_res.scalar_one_or_none() + + # Ha nincs rögzített árfolyam, 1.0-val számolunk (vagy hibát dobhatunk a konfigurációtól függően) + exchange_rate = rate_obj.rate if rate_obj else Decimal("1.0") + + # EUR kalkuláció: Helyi összeg / Árfolyam (Pl. 40000 HUF / 400 = 100 EUR) + amt_eur = Decimal(str(cost_in.amount_local)) / exchange_rate if exchange_rate > 0 else Decimal("0") + + # 2. Költség rekord létrehozása + new_cost = AssetCost( + asset_id=cost_in.asset_id, + organization_id=cost_in.organization_id, + driver_id=user_id, + cost_type=cost_in.cost_type, + amount_local=cost_in.amount_local, + currency_local=cost_in.currency_local, + amount_eur=amt_eur, + net_amount_local=cost_in.net_amount_local, + vat_rate=cost_in.vat_rate, + exchange_rate_used=exchange_rate, + mileage_at_cost=cost_in.mileage_at_cost, + date=cost_in.date or datetime.now(), + data=cost_in.data or {} + ) + db.add(new_cost) + + # 3. Telemetria frissítése (Ha érkezett kilométeróra állás) + if cost_in.mileage_at_cost: + tel_stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == cost_in.asset_id) + res = await db.execute(tel_stmt) + telemetry = res.scalar_one_or_none() + + if telemetry: + # Megakadályozzuk a "visszatekerést" + if cost_in.mileage_at_cost > (telemetry.current_mileage or 0): + telemetry.current_mileage = cost_in.mileage_at_cost + else: + # Ha még nem volt telemetria adat, létrehozzuk + new_telemetry = AssetTelemetry( + asset_id=cost_in.asset_id, + current_mileage=cost_in.mileage_at_cost + ) + db.add(new_telemetry) + + # 4. Gamification XP jóváírás + xp_reward = await self.get_param(db, "XP_PER_COST_LOG", 50) + stats_stmt = select(UserStats).where(UserStats.user_id == user_id) + stats_res = await db.execute(stats_stmt) + user_stats = stats_res.scalar_one_or_none() + + if user_stats: + user_stats.total_xp += int(xp_reward) + logger.info(f"User {user_id} earned {xp_reward} XP for cost logging.") + + await db.commit() + await db.refresh(new_cost) + return new_cost + + except Exception as e: + await db.rollback() + logger.error(f"Error in record_cost: {str(e)}") + raise e + +cost_service = CostService() \ No newline at end of file diff --git a/backend/app/services/email_manager.py b/backend/app/services/email_manager.py index 8a73ed9..a698414 100755 --- a/backend/app/services/email_manager.py +++ b/backend/app/services/email_manager.py @@ -17,6 +17,9 @@ class EmailManager: button_text = locale_manager.get(f"email.{template_key}_button", lang=lang) footer = locale_manager.get(f"email.{template_key}_footer", lang=lang) + # ÚJ: A link fallback szöveg is a nyelvi fájlból jön + link_fallback_text = locale_manager.get("email.link_fallback", lang=lang) + return f""" @@ -30,8 +33,8 @@ class EmailManager:

- Ha a gomb nem működik, másolja be ezt a linket a böngészőjébe:
- {variables.get('link')} + {link_fallback_text}
+ {variables.get('link')}


{footer}

@@ -66,18 +69,11 @@ class EmailManager: response = sg.send(message) logger.info(f"SendGrid Status: {response.status_code} for {recipient}") - if response.status_code >= 400: - logger.error(f"SendGrid Hibaüzenet: {response.body}") - return {"status": "success", "provider": "sendgrid", "code": response.status_code} except Exception as e: logger.error(f"SendGrid Kritikus Hiba: {str(e)}") # 2. SMTP Fallback - if not settings.SMTP_HOST or not settings.SMTP_USER or not settings.SMTP_PASSWORD: - logger.warning("SMTP nincs konfigurálva a fallback-hez.") - return {"status": "error", "message": "Nincs elérhető szolgáltató."} - try: msg = MIMEMultipart() msg["From"] = f"{settings.EMAILS_FROM_NAME} <{settings.EMAILS_FROM_EMAIL}>" diff --git a/backend/app/services/gamification_service.py b/backend/app/services/gamification_service.py index 87c8caa..1789d59 100755 --- a/backend/app/services/gamification_service.py +++ b/backend/app/services/gamification_service.py @@ -1,26 +1,47 @@ from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, text +from sqlalchemy import select from app.models.gamification import UserStats, PointsLedger -from app.models.identity import User +import math class GamificationService: @staticmethod - async def award_points(db: AsyncSession, user_id: int, points: int, reason: str): - """Pontok jóváírása (SQL szinkronizált points mezővel).""" - new_entry = PointsLedger( - user_id=user_id, - points=points, # Javítva: points_change helyett points - reason=reason - ) - db.add(new_entry) - - result = await db.execute(select(UserStats).where(UserStats.user_id == user_id)) - stats = result.scalar_one_or_none() + async def process_activity(db: AsyncSession, user_id: int, xp_amount: int, social_amount: int, reason: str): + """ + XP növelés, Szintlépés csekk és Automata Kredit váltás. + """ + # 1. User statisztika lekérése + stmt = select(UserStats).where(UserStats.user_id == user_id) + stats = (await db.execute(stmt)).scalar_one_or_none() if not stats: - stats = UserStats(user_id=user_id, total_points=0, current_level=1) + stats = UserStats(user_id=user_id, total_xp=0, social_points=0, current_level=1, credits=0) db.add(stats) + + # 2. Részletes Logolás (PointsLedger) - A visszakövethetőség miatt + db.add(PointsLedger( + user_id=user_id, + xp_gain=xp_amount, + social_gain=social_amount, + reason=reason + )) + + # 3. XP és Szintlépés (Nehezedő görbe) + stats.total_xp += xp_amount + # Képlet: Level = (XP / 500)^(1/1.5) + new_level = int((stats.total_xp / 500) ** (1/1.5)) + 1 + if new_level > stats.current_level: + stats.current_level = new_level + + # 4. Automata Kredit váltás + # Példa: Minden 100 Social pont automatikusan 1 Kredit lesz + stats.social_points += social_amount + if stats.social_points >= 100: + new_credits = stats.social_points // 100 + stats.credits += new_credits + stats.social_points %= 100 # A maradék megmarad a következő váltáshoz - stats.total_points += points - await db.flush() - return stats.total_points \ No newline at end of file + # Külön log a váltásról + db.add(PointsLedger(user_id=user_id, reason=f"Auto-conversion: {new_credits} Credits", credits_change=new_credits)) + + await db.commit() + return stats \ No newline at end of file diff --git a/backend/app/services/harvester_base.py b/backend/app/services/harvester_base.py index ebed6a8..ff23818 100644 --- a/backend/app/services/harvester_base.py +++ b/backend/app/services/harvester_base.py @@ -1,34 +1,45 @@ +# /app/services/harvester_base.py import httpx +import logging from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select -from app.models.vehicle import VehicleCatalog +from app.models.asset import AssetCatalog + +logger = logging.getLogger(__name__) class BaseHarvester: def __init__(self, category: str): - self.category = category + self.category = category # car, bike, truck self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.0"} - async def check_exists(self, db: AsyncSession, brand: str, model: str): - """Ellenőrzi, hogy az adott modell létezik-e már.""" - stmt = select(VehicleCatalog).where( - VehicleCatalog.brand == brand, - VehicleCatalog.model == model, - VehicleCatalog.category == self.category + async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None): + """Ellenőrzi a katalógusban való létezést.""" + stmt = select(AssetCatalog).where( + AssetCatalog.make == brand, + AssetCatalog.model == model, + AssetCatalog.vehicle_class == self.category ) + if gen: + stmt = stmt.where(AssetCatalog.generation == gen) + result = await db.execute(stmt) return result.scalar_one_or_none() - async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict = None): - """Létrehoz vagy frissít egy katalógus bejegyzést.""" - existing = await self.check_exists(db, brand, model) + async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict): + """Létrehoz vagy frissít egy bejegyzést az AssetCatalog-ban.""" + existing = await self.check_exists(db, brand, model, specs.get("generation")) if not existing: - new_v = VehicleCatalog( - brand=brand, + new_v = AssetCatalog( + make=brand, model=model, - category=self.category, - factory_specs=specs or {}, - verification_status="incomplete" if not specs else "verified" + generation=specs.get("generation"), + year_from=specs.get("year_from"), + year_to=specs.get("year_to"), + vehicle_class=self.category, + fuel_type=specs.get("fuel_type"), + engine_code=specs.get("engine_code") ) db.add(new_v) + logger.info(f"🆕 Új katalógus elem: {brand} {model}") return True return False \ No newline at end of file diff --git a/backend/app/services/recon_bot.py b/backend/app/services/recon_bot.py new file mode 100644 index 0000000..1af6d7b --- /dev/null +++ b/backend/app/services/recon_bot.py @@ -0,0 +1,51 @@ +import asyncio +import logging +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.models.asset import Asset, AssetCatalog, AssetTelemetry + +logger = logging.getLogger(__name__) + +async def run_vehicle_recon(db: AsyncSession, asset_id: str): + """ + VIN alapján megkeresi a mélységi adatokat és frissíti a Digitális Ikert. + """ + # 1. Lekérjük a járművet és a katalógusát + stmt = select(Asset).where(Asset.id == asset_id) + result = await db.execute(stmt) + asset = result.scalar_one_or_none() + + if not asset or not asset.catalog_id: + return False + + logger.info(f"🤖 Robot indul: {asset.vin} felderítése...") + + # 2. SZIMULÁLT ADATGYŰJTÉS (Itt hívnánk meg az API-kat: NHTSA, autodna stb.) + await asyncio.sleep(2) # Időigényes keresés szimulálása + + deep_data = { + "assembly_plant": "Fremont, California", + "drive_unit": "Dual Motor - Raven type", + "onboard_charger": "11 kW", + "supercharging_max": "250 kW", + "safety_rating": "5-star EuroNCAP" + } + + # 3. Katalógus frissítése + catalog_stmt = select(AssetCatalog).where(AssetCatalog.id == asset.catalog_id) + catalog = (await db.execute(catalog_stmt)).scalar_one_or_none() + + if catalog: + current_data = catalog.factory_data or {} + current_data.update(deep_data) + catalog.factory_data = current_data + + # 4. Telemetria frissítése (A robot talált egy visszahívást, VQI csökken kicsit) + telemetry_stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id) + telemetry = (await db.execute(telemetry_stmt)).scalar_one_or_none() + if telemetry: + telemetry.vqi_score = 99.2 # Robot frissített állapota + + await db.commit() + logger.info(f"✨ Robot végzett: {asset.license_plate} felokosítva.") + return True \ No newline at end of file diff --git a/backend/app/services/robot_manager.py b/backend/app/services/robot_manager.py index d2dbe7d..92875e1 100644 --- a/backend/app/services/robot_manager.py +++ b/backend/app/services/robot_manager.py @@ -1,40 +1,37 @@ +# /app/services/robot_manager.py import asyncio +import logging from datetime import datetime -# Frissített importok az új fájlnevekhez: from .harvester_cars import CarHarvester -from .harvester_bikes import BikeHarvester -from .harvester_trucks import TruckHarvester +# Megjegyzés: Ellenőrizd, hogy a harvester_bikes/trucks fájlokban is BaseHarvester az alap! + +logger = logging.getLogger(__name__) class RobotManager: @staticmethod async def run_full_sync(db): - """Sorban lefuttatja az összes robotot.""" - print(f"🕒 Szinkronizáció indítva: {datetime.now()}") + """Sorban lefuttatja a robotokat az új AssetCatalog struktúrához.""" + logger.info(f"🕒 Teljes szinkronizáció indítva: {datetime.now()}") robots = [ CarHarvester(), - BikeHarvester(), - TruckHarvester() + # BikeHarvester(), + # TruckHarvester() ] for robot in robots: try: - # Itt a robot lekéri az API-tól az ÖSSZES márkát és frissít await robot.run(db) + logger.info(f"✅ {robot.category} robot sikeresen lefutott.") await asyncio.sleep(5) except Exception as e: - print(f"❌ Hiba a {robot.category} robotnál: {e}") + logger.error(f"❌ Kritikus hiba a {robot.category} robotnál: {e}") @staticmethod async def schedule_nightly_run(db): - """ - Egyszerű ciklus, ami figyeli az időt. - Ha éjjel 2 óra van, elindítja a teljes szinkront. - """ while True: now = datetime.now() - # Ha hajnali 2 és 2:01 között vagyunk, indítás if now.hour == 2 and now.minute == 0: await RobotManager.run_full_sync(db) - await asyncio.sleep(70) # Várunk, hogy ne induljon el többször ugyanabban a percben - await asyncio.sleep(30) # 30 másodpercenként ellenőrizzük az időt \ No newline at end of file + await asyncio.sleep(70) + await asyncio.sleep(30) \ No newline at end of file diff --git a/backend/migrations/__pycache__/env.cpython-312.pyc b/backend/migrations/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..952453cb41d780cc0b25791bd1d184a614204581 GIT binary patch literal 3226 zcmZ`*U2NOd6~33GD2kFr`A4#2`A1P?C(0UoZPa8-w{%wRwp#{cXtRfEzywO9U1}7i zUeZaNC4>3o~-UG^Euw><8*&n$O(?^(*tEOCt{>>rIMfhcY9o05!G<%yGQ+<8DZQ@NBeK} zF-T>;y>AV>c=*alDRXpeF0KSWK)Dem1ou%TqV#;g<;EbMSh{!=z1DJ(w;aTejv`Kvbk&&^Qy6^8ESrkRLc3Hhw+1D zsn@LQ+_YS+0$4Cr%hHTRlLpAjrn)o?!Qa#hazehI(8|>cu^xY_KmOD-;Bg^wJ#4}BEFqr_ZAHy8!~>J&mmx`9d#;P6dhXz$x-nN4%^w$G9a zx@vQZY{{2lHM~hDk<&h#uM|pZ(Xu&1E~|iBuBv%EK-2{tysBc0#Y(v<6E$yDOuC!| zZ3i*>h0|qik;s--G0bVzxH45;w!@lH)Rz=B??xs4Fk&1oa|CWHsNoMi@c+X<*pQ<4 zZ{EB4kgrLTo5ExxDD6Q%Dz-RJZA3N0`Q8mMR*}{2E49us1$pfTvN+FHu*J7+<`LWB zYgY`xLv)@VvnXr3kdCT#P}K5}j$VM4ba59QVOEQAkcG=OUtKEbS`oG#w$(6Thhg(< z$&$&UYURtSCDR1ilDCU)mP}_G?SU&QF#(@H$9WGesqBSns=pWq~ zI@uT+*%_Q@j86O~$ou%O!bt3`2a>fw^7BAyN092mSWOt)77|~xNSvq#Qnf(pzzjm9 zzeZ=G?0VEUGv+VCYrE0R--c`NH?XWrkd5x_TkY(Zx=%-Ia93}e1MRp-Yi9~L(q=&E zTQdBJS)<&8b`!z*>T&JvyT^eHx{Vd|Ye0X@tg^rL`Or;#1DB+3tZQ2l&*v%AZQzbG zl66OZlTWvq!;vvv3q4ZbXx>`d@mebZT|?*5M?ZmU8m{A%n@&tPD<3;^o=3hzI{49x zt_}h^@hA2wPP6mZgI%JOW`M&W4%#(4Kozm5%chwY2~CB~yBxHGl+0#}+ zBlM`+!u*o1{|sW-R5$5aum#FHs;>^?Y`>zJ)rzUYq`?-1s+h}IN`pj;veCnQ%~uQ^ z)R~+H2(8xUeYpJ#?E`cBPVWfex-eW5hCex1k7sJ}%%2Ne!uK1}$WC8u^~~?ieC6l* z`ISpe5k=y4DOr<}bt$zir9KlI;h}o?crAQ4TpAhLG8BQp`Sp;J+rN4xVx22j&8->HV4h><{sJ6lW9sdW@y^v18du zcKs!sP4eq!IclE|f`5a-*%Y_I4pMsA6|71$!y-uvmZ1LT z%Q8q{rRpecXm;lEOm^n&x%|)H`l%ghQIi(>yi@)JPl7ul>QK?463DWduPiK3Zsq9` zNZgskk?zhUgqAuoO1oI#+c;rTmKYoM&<5 zxfsC5_hJH0GzSnDUpeoN+Ga-@Ty*7)mgI-}>Y-#Ul-xXVb}N)=fc(dXp#n4pUHNZ< z{GYfx*e(DKVeGE|a0%E9q~NoBk(D>@T)K6s7Ma+Jocx?Swck`R68fJb28&;Cfz8n5 z!}lH*H^tN2+; None: - """Aszinkron kapcsolat felépítése és migráció futtatása""" connectable = async_engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool, ) - async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) - await connectable.dispose() if context.is_offline_mode(): - url = config.get_main_option("sqlalchemy.url") context.configure( - url=url, + url=config.get_main_option("sqlalchemy.url"), target_metadata=target_metadata, - literal_binds=True, - include_schemas=True + literal_binds=True, + include_schemas=True, + include_object=include_object ) with context.begin_transaction(): context.run_migrations() diff --git a/backend/migrations/versions/0adbe75a0b3f_complete_sync.py b/backend/migrations/versions/0adbe75a0b3f_complete_sync.py new file mode 100644 index 0000000..68163e6 --- /dev/null +++ b/backend/migrations/versions/0adbe75a0b3f_complete_sync.py @@ -0,0 +1,554 @@ +"""complete_sync + +Revision ID: 0adbe75a0b3f +Revises: +Create Date: 2026-02-09 17:49:12.955967 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '0adbe75a0b3f' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('badges', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=False), + sa.Column('icon_url', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name'), + schema='data' + ) + op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False, schema='data') + op.create_table('exchange_rates', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('base_currency', sa.String(length=3), nullable=True), + sa.Column('target_currency', sa.String(length=3), nullable=True), + sa.Column('rate', sa.Numeric(precision=18, scale=6), nullable=True), + sa.Column('rate_date', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('geo_postal_codes', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('country_code', sa.String(length=5), nullable=True), + sa.Column('zip_code', sa.String(length=10), nullable=False), + sa.Column('city', sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('geo_street_types', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name'), + schema='data' + ) + op.create_table('level_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('level_number', sa.Integer(), nullable=False), + sa.Column('min_points', sa.Integer(), nullable=False), + sa.Column('rank_name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('level_number'), + schema='data' + ) + op.create_index(op.f('ix_data_level_configs_id'), 'level_configs', ['id'], unique=False, schema='data') + op.create_table('point_rules', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('action_key', sa.String(), nullable=False), + sa.Column('points', sa.Integer(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True, schema='data') + op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False, schema='data') + op.create_table('regional_settings', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('country_code', sa.String(), nullable=False), + sa.Column('currency', sa.String(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('country_code'), + schema='data' + ) + op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False, schema='data') + op.create_table('service_specialties', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('parent_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(), nullable=False), + sa.Column('slug', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['parent_id'], ['data.service_specialties.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('slug'), + schema='data' + ) + op.create_table('subscription_tiers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('rules', sa.JSON(), nullable=True), + sa.Column('is_custom', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name'), + schema='data' + ) + op.create_table('system_parameters', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('key', sa.String(length=50), nullable=True), + sa.Column('value', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_system_parameters_id'), 'system_parameters', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_system_parameters_key'), 'system_parameters', ['key'], unique=True, schema='data') + op.create_table('vehicle_catalog', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('make', sa.String(), nullable=False), + sa.Column('model', sa.String(), nullable=False), + sa.Column('generation', sa.String(), nullable=True), + sa.Column('year_from', sa.Integer(), nullable=True), + sa.Column('year_to', sa.Integer(), nullable=True), + sa.Column('vehicle_class', sa.String(), nullable=True), + sa.Column('fuel_type', sa.String(), nullable=True), + sa.Column('engine_code', sa.String(), nullable=True), + sa.Column('factory_data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_vehicle_catalog_id'), 'vehicle_catalog', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_vehicle_catalog_make'), 'vehicle_catalog', ['make'], unique=False, schema='data') + op.create_index(op.f('ix_data_vehicle_catalog_model'), 'vehicle_catalog', ['model'], unique=False, schema='data') + op.create_table('addresses', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('postal_code_id', sa.Integer(), nullable=True), + sa.Column('street_name', sa.String(length=200), nullable=False), + sa.Column('street_type', sa.String(length=50), nullable=False), + sa.Column('house_number', sa.String(length=50), nullable=False), + sa.Column('stairwell', sa.String(length=20), nullable=True), + sa.Column('floor', sa.String(length=20), nullable=True), + sa.Column('door', sa.String(length=20), nullable=True), + sa.Column('parcel_id', sa.String(length=50), nullable=True), + sa.Column('full_address_text', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['postal_code_id'], ['data.geo_postal_codes.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('assets', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('vin', sa.String(length=17), nullable=False), + sa.Column('license_plate', sa.String(length=20), nullable=True), + sa.Column('name', sa.String(), nullable=True), + sa.Column('year_of_manufacture', sa.Integer(), nullable=True), + sa.Column('catalog_id', sa.Integer(), nullable=True), + sa.Column('is_verified', sa.Boolean(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['catalog_id'], ['data.vehicle_catalog.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_assets_license_plate'), 'assets', ['license_plate'], unique=False, schema='data') + op.create_index(op.f('ix_data_assets_vin'), 'assets', ['vin'], unique=True, schema='data') + op.create_table('geo_streets', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('postal_code_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(length=200), nullable=False), + sa.ForeignKeyConstraint(['postal_code_id'], ['data.geo_postal_codes.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('asset_events', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('asset_id', sa.UUID(), nullable=False), + sa.Column('event_type', sa.String(length=50), nullable=False), + sa.Column('recorded_mileage', sa.Integer(), nullable=True), + sa.Column('data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('asset_financials', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('asset_id', sa.UUID(), nullable=True), + sa.Column('acquisition_price', sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column('acquisition_date', sa.DateTime(), nullable=True), + sa.Column('financing_type', sa.String(), nullable=True), + sa.Column('residual_value_estimate', sa.Numeric(precision=18, scale=2), nullable=True), + sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('asset_id'), + schema='data' + ) + op.create_table('asset_telemetry', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('asset_id', sa.UUID(), nullable=True), + sa.Column('current_mileage', sa.Integer(), nullable=True), + sa.Column('mileage_unit', sa.String(length=10), nullable=True), + sa.Column('vqi_score', sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column('dbs_score', sa.Numeric(precision=5, scale=2), nullable=True), + sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('asset_id'), + schema='data' + ) + op.create_table('persons', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('id_uuid', sa.UUID(), nullable=False), + sa.Column('address_id', sa.UUID(), nullable=True), + sa.Column('last_name', sa.String(), nullable=False), + sa.Column('first_name', sa.String(), nullable=False), + sa.Column('mothers_last_name', sa.String(), nullable=True), + sa.Column('mothers_first_name', sa.String(), nullable=True), + sa.Column('birth_place', sa.String(), nullable=True), + sa.Column('birth_date', sa.DateTime(), nullable=True), + sa.Column('phone', sa.String(), nullable=True), + sa.Column('identity_docs', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.Column('medical_emergency', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.Column('ice_contact', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['address_id'], ['data.addresses.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id_uuid'), + schema='data' + ) + op.create_index(op.f('ix_data_persons_id'), 'persons', ['id'], unique=False, schema='data') + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('hashed_password', sa.String(), nullable=True), + sa.Column('role', sa.Enum('admin', 'user', 'service', 'fleet_manager', 'driver', name='userrole'), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('region_code', sa.String(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('person_id', sa.BigInteger(), nullable=True), + sa.Column('preferred_language', sa.String(length=5), nullable=True), + sa.Column('preferred_currency', sa.String(length=3), nullable=True), + sa.Column('timezone', sa.String(length=50), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['person_id'], ['data.persons.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_users_email'), 'users', ['email'], unique=True, schema='data') + op.create_index(op.f('ix_data_users_id'), 'users', ['id'], unique=False, schema='data') + op.create_table('asset_reviews', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('asset_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('overall_rating', sa.Integer(), nullable=True), + sa.Column('criteria_scores', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.Column('comment', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('audit_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('target_type', sa.String(), nullable=True), + sa.Column('target_id', sa.String(), nullable=True), + sa.Column('action', sa.String(), nullable=False), + sa.Column('changes', sa.JSON(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_audit_logs_id'), 'audit_logs', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_audit_logs_target_id'), 'audit_logs', ['target_id'], unique=False, schema='data') + op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], unique=False, schema='data') + op.create_table('documents', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('parent_type', sa.String(length=20), nullable=False), + sa.Column('parent_id', sa.String(length=50), nullable=False), + sa.Column('doc_type', sa.String(length=50), nullable=True), + sa.Column('original_name', sa.String(length=255), nullable=False), + sa.Column('file_hash', sa.String(length=64), nullable=False), + sa.Column('file_ext', sa.String(length=10), nullable=True), + sa.Column('mime_type', sa.String(length=100), nullable=True), + sa.Column('file_size', sa.Integer(), nullable=True), + sa.Column('has_thumbnail', sa.Boolean(), nullable=True), + sa.Column('thumbnail_path', sa.String(length=255), nullable=True), + sa.Column('uploaded_by', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['uploaded_by'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('organizations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('address_id', sa.UUID(), nullable=True), + sa.Column('full_name', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('display_name', sa.String(length=50), nullable=True), + sa.Column('default_currency', sa.String(length=3), nullable=True), + sa.Column('country_code', sa.String(length=2), nullable=True), + sa.Column('language', sa.String(length=5), nullable=True), + sa.Column('address_zip', sa.String(length=10), nullable=True), + sa.Column('address_city', sa.String(length=100), nullable=True), + sa.Column('address_street_name', sa.String(length=150), nullable=True), + sa.Column('address_street_type', sa.String(length=50), nullable=True), + sa.Column('address_house_number', sa.String(length=20), nullable=True), + sa.Column('address_hrsz', sa.String(length=50), nullable=True), + sa.Column('address_stairwell', sa.String(length=20), nullable=True), + sa.Column('address_floor', sa.String(length=20), nullable=True), + sa.Column('address_door', sa.String(length=20), nullable=True), + sa.Column('tax_number', sa.String(length=20), nullable=True), + sa.Column('reg_number', sa.String(length=50), nullable=True), + sa.Column('org_type', postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), nullable=True), + sa.Column('status', sa.String(length=30), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('notification_settings', sa.JSON(), server_default=sa.text('\'{ "notify_owner": true, "alert_days_before": [30, 15, 7, 1] }\'::jsonb'), nullable=True), + sa.Column('external_integration_config', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.Column('owner_id', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_transferable', sa.Boolean(), nullable=True), + sa.Column('is_verified', sa.Boolean(), nullable=True), + sa.Column('verification_expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['address_id'], ['data.addresses.id'], ), + sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_organizations_id'), 'organizations', ['id'], unique=False, schema='data') + op.create_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=True, schema='data') + op.create_table('points_ledger', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('points', sa.Integer(), nullable=False), + sa.Column('reason', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False, schema='data') + op.create_table('ratings', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('author_id', sa.Integer(), nullable=False), + sa.Column('target_type', sa.String(length=20), nullable=False), + sa.Column('target_id', sa.UUID(), nullable=False), + sa.Column('score', sa.Integer(), nullable=False), + sa.Column('comment', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['author_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('user_badges', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('badge_id', sa.Integer(), nullable=False), + sa.Column('earned_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['badge_id'], ['data.badges.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_user_badges_id'), 'user_badges', ['id'], unique=False, schema='data') + op.create_table('user_stats', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('total_xp', sa.Integer(), nullable=False), + sa.Column('social_points', sa.Integer(), nullable=False), + sa.Column('current_level', sa.Integer(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('user_id'), + schema='data' + ) + op.create_table('vehicle_ownerships', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('vehicle_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('start_date', sa.Date(), nullable=False), + sa.Column('end_date', sa.Date(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.ForeignKeyConstraint(['vehicle_id'], ['data.assets.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data') + op.create_table('verification_tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('token', sa.UUID(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('token_type', sa.String(length=20), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('is_used', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token'), + schema='data' + ) + op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False, schema='data') + op.create_table('wallets', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('coin_balance', sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column('credit_balance', sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column('currency', sa.String(length=3), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id'), + schema='data' + ) + op.create_index(op.f('ix_data_wallets_id'), 'wallets', ['id'], unique=False, schema='data') + op.create_table('asset_assignments', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('asset_id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('assigned_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('released_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('status', sa.String(length=30), nullable=True), + sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ), + sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('asset_costs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('asset_id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('driver_id', sa.Integer(), nullable=True), + sa.Column('cost_type', sa.String(length=50), nullable=False), + sa.Column('amount', sa.Numeric(precision=18, scale=2), nullable=False), + sa.Column('currency', sa.String(length=3), nullable=True), + sa.Column('net_amount', sa.Numeric(precision=18, scale=2), nullable=True), + sa.Column('vat_rate', sa.Numeric(precision=5, scale=2), nullable=True), + sa.Column('exchange_rate_at_cost', sa.Numeric(precision=18, scale=6), nullable=True), + sa.Column('date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('mileage_at_cost', sa.Integer(), nullable=True), + sa.Column('data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ), + sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], ), + sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('credit_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('org_id', sa.Integer(), nullable=True), + sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('org_subscriptions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('org_id', sa.Integer(), nullable=True), + sa.Column('tier_id', sa.Integer(), nullable=True), + sa.Column('valid_from', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('valid_until', sa.DateTime(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ), + sa.ForeignKeyConstraint(['tier_id'], ['data.subscription_tiers.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_table('organization_members', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('role', sa.String(), nullable=True), + sa.Column('permissions', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), + sa.PrimaryKeyConstraint('id'), + schema='data' + ) + op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False, schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members', schema='data') + op.drop_table('organization_members', schema='data') + op.drop_table('org_subscriptions', schema='data') + op.drop_table('credit_logs', schema='data') + op.drop_table('asset_costs', schema='data') + op.drop_table('asset_assignments', schema='data') + op.drop_index(op.f('ix_data_wallets_id'), table_name='wallets', schema='data') + op.drop_table('wallets', schema='data') + op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens', schema='data') + op.drop_table('verification_tokens', schema='data') + op.drop_index(op.f('ix_data_vehicle_ownerships_id'), table_name='vehicle_ownerships', schema='data') + op.drop_table('vehicle_ownerships', schema='data') + op.drop_table('user_stats', schema='data') + op.drop_index(op.f('ix_data_user_badges_id'), table_name='user_badges', schema='data') + op.drop_table('user_badges', schema='data') + op.drop_table('ratings', schema='data') + op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger', schema='data') + op.drop_table('points_ledger', schema='data') + op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations', schema='data') + op.drop_index(op.f('ix_data_organizations_id'), table_name='organizations', schema='data') + op.drop_table('organizations', schema='data') + op.drop_table('documents', schema='data') + op.drop_index(op.f('ix_data_audit_logs_target_type'), table_name='audit_logs', schema='data') + op.drop_index(op.f('ix_data_audit_logs_target_id'), table_name='audit_logs', schema='data') + op.drop_index(op.f('ix_data_audit_logs_id'), table_name='audit_logs', schema='data') + op.drop_table('audit_logs', schema='data') + op.drop_table('asset_reviews', schema='data') + op.drop_index(op.f('ix_data_users_id'), table_name='users', schema='data') + op.drop_index(op.f('ix_data_users_email'), table_name='users', schema='data') + op.drop_table('users', schema='data') + op.drop_index(op.f('ix_data_persons_id'), table_name='persons', schema='data') + op.drop_table('persons', schema='data') + op.drop_table('asset_telemetry', schema='data') + op.drop_table('asset_financials', schema='data') + op.drop_table('asset_events', schema='data') + op.drop_table('geo_streets', schema='data') + op.drop_index(op.f('ix_data_assets_vin'), table_name='assets', schema='data') + op.drop_index(op.f('ix_data_assets_license_plate'), table_name='assets', schema='data') + op.drop_table('assets', schema='data') + op.drop_table('addresses', schema='data') + op.drop_index(op.f('ix_data_vehicle_catalog_model'), table_name='vehicle_catalog', schema='data') + op.drop_index(op.f('ix_data_vehicle_catalog_make'), table_name='vehicle_catalog', schema='data') + op.drop_index(op.f('ix_data_vehicle_catalog_id'), table_name='vehicle_catalog', schema='data') + op.drop_table('vehicle_catalog', schema='data') + op.drop_index(op.f('ix_data_system_parameters_key'), table_name='system_parameters', schema='data') + op.drop_index(op.f('ix_data_system_parameters_id'), table_name='system_parameters', schema='data') + op.drop_table('system_parameters', schema='data') + op.drop_table('subscription_tiers', schema='data') + op.drop_table('service_specialties', schema='data') + op.drop_index(op.f('ix_data_regional_settings_id'), table_name='regional_settings', schema='data') + op.drop_table('regional_settings', schema='data') + op.drop_index(op.f('ix_data_point_rules_id'), table_name='point_rules', schema='data') + op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules', schema='data') + op.drop_table('point_rules', schema='data') + op.drop_index(op.f('ix_data_level_configs_id'), table_name='level_configs', schema='data') + op.drop_table('level_configs', schema='data') + op.drop_table('geo_street_types', schema='data') + op.drop_table('geo_postal_codes', schema='data') + op.drop_table('exchange_rates', schema='data') + op.drop_index(op.f('ix_data_badges_id'), table_name='badges', schema='data') + op.drop_table('badges', schema='data') + # ### end Alembic commands ### diff --git a/backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py b/backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py new file mode 100644 index 0000000..49204fa --- /dev/null +++ b/backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py @@ -0,0 +1,222 @@ +"""fix_identity_scope_and_finalize_asset_costs + +Revision ID: 2cfe9285eb9d +Revises: 0adbe75a0b3f +Create Date: 2026-02-10 09:47:16.879385 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '2cfe9285eb9d' +down_revision: Union[str, Sequence[str], None] = '0adbe75a0b3f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey') + op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey') + op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey') + op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.add_column('asset_costs', sa.Column('amount_local', sa.Numeric(precision=18, scale=2), nullable=False)) + op.add_column('asset_costs', sa.Column('currency_local', sa.String(length=3), nullable=False)) + op.add_column('asset_costs', sa.Column('amount_eur', sa.Numeric(precision=18, scale=2), nullable=True)) + op.add_column('asset_costs', sa.Column('net_amount_local', sa.Numeric(precision=18, scale=2), nullable=True)) + op.add_column('asset_costs', sa.Column('exchange_rate_used', sa.Numeric(precision=18, scale=6), nullable=True)) + op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey') + op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey') + op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_column('asset_costs', 'currency') + op.drop_column('asset_costs', 'amount') + op.drop_column('asset_costs', 'exchange_rate_at_cost') + op.drop_column('asset_costs', 'net_amount') + op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey') + op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey') + op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey') + op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey') + op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey') + op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey') + op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey') + op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey') + op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey') + op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('exchange_rates', 'rate', + existing_type=sa.NUMERIC(precision=18, scale=6), + nullable=False) + op.create_unique_constraint(None, 'exchange_rates', ['target_currency'], schema='data') + op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey') + op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey') + op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey') + op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey') + op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + existing_nullable=True) + op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey') + op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') + op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey') + op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey') + op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey') + op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey') + op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('system_parameters', 'key', + existing_type=sa.VARCHAR(length=50), + nullable=False) + op.alter_column('system_parameters', 'value', + existing_type=postgresql.JSON(astext_type=sa.Text()), + nullable=False) + op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey') + op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey') + op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey') + op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.alter_column('users', 'custom_permissions', + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=sa.JSON(), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey') + op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') + op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey') + op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE') + op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') + op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id']) + op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'users', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id']) + op.alter_column('users', 'custom_permissions', + existing_type=sa.JSON(), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=True, + existing_server_default=sa.text("'{}'::jsonb")) + op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id']) + op.alter_column('system_parameters', 'value', + existing_type=postgresql.JSON(astext_type=sa.Text()), + nullable=True) + op.alter_column('system_parameters', 'key', + existing_type=sa.VARCHAR(length=50), + nullable=True) + op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id']) + op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id']) + op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'persons', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id']) + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) + op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id']) + op.alter_column('organizations', 'org_type', + existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), + type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'), + existing_nullable=True) + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id']) + op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id']) + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id']) + op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id']) + op.drop_constraint(None, 'exchange_rates', schema='data', type_='unique') + op.alter_column('exchange_rates', 'rate', + existing_type=sa.NUMERIC(precision=18, scale=6), + nullable=True) + op.drop_constraint(None, 'documents', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id']) + op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id']) + op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'assets', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id']) + op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id']) + op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id']) + op.add_column('asset_costs', sa.Column('net_amount', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=True)) + op.add_column('asset_costs', sa.Column('exchange_rate_at_cost', sa.NUMERIC(precision=18, scale=6), autoincrement=False, nullable=True)) + op.add_column('asset_costs', sa.Column('amount', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=False)) + op.add_column('asset_costs', sa.Column('currency', sa.VARCHAR(length=3), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id']) + op.drop_column('asset_costs', 'exchange_rate_used') + op.drop_column('asset_costs', 'net_amount_local') + op.drop_column('asset_costs', 'amount_eur') + op.drop_column('asset_costs', 'currency_local') + op.drop_column('asset_costs', 'amount_local') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id']) + op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id']) + op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey') + op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id']) + # ### end Alembic commands ### diff --git a/backend/migrations/versions/__pycache__/0adbe75a0b3f_complete_sync.cpython-312.pyc b/backend/migrations/versions/__pycache__/0adbe75a0b3f_complete_sync.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5e1118d15ece983c61abf517cbe5afef29c9278 GIT binary patch literal 49482 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 O|LwIrIKHJ9Ecm~Q@M|sr literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/__pycache__/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.cpython-312.pyc b/backend/migrations/versions/__pycache__/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83cdc7a1351659e902233cd96aa6481c9f7b8584 GIT binary patch literal 22622 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 literal 0 HcmV?d00001 diff --git a/backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py b/backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py deleted file mode 100755 index 82ee655..0000000 --- a/backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Clean gamification setup - -Revision ID: c21c2c7e70d4 -Revises: -Create Date: 2026-01-24 11:19:10.464212 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'c21c2c7e70d4' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('audit_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('target_type', sa.String(), nullable=True), - sa.Column('target_id', sa.Integer(), nullable=True), - sa.Column('action', sa.String(), nullable=False), - sa.Column('changes', sa.JSON(), nullable=True), - sa.Column('ip_address', sa.String(), nullable=True), - sa.Column('user_agent', sa.String(), nullable=True), - sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_audit_logs_id'), 'audit_logs', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_audit_logs_target_id'), 'audit_logs', ['target_id'], unique=False, schema='data') - op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], unique=False, schema='data') - op.create_table('companies', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('subscription_tier', sa.String(), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') - op.create_table('company_members', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role', sa.Enum('OWNER', 'MANAGER', 'DRIVER', name='companyrole'), nullable=False), - sa.Column('can_edit_service', sa.Boolean(), nullable=True), - sa.Column('can_see_costs', sa.Boolean(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') - op.create_table('vehicle_assignments', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=False), - sa.Column('vehicle_id', sa.Integer(), nullable=False), - sa.Column('driver_id', sa.Integer(), nullable=False), - sa.Column('start_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('start_odometer', sa.Integer(), nullable=True), - sa.Column('end_odometer', sa.Integer(), nullable=True), - sa.Column('notes', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], ), - sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], ), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_assignments_id'), 'vehicle_assignments', ['id'], unique=False, schema='data') - op.create_table('vehicle_ownerships', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vehicle_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('start_date', sa.Date(), nullable=False), - sa.Column('end_date', sa.Date(), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data') - # op.drop_table('costs', schema='data') - # op.drop_table('alembic_version') - # op.drop_table('vehicle_history', schema='data') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('vehicle_history', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('start_mileage', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('end_mileage', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), sa.Computed('(end_date IS NULL)', persisted=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('vehicle_history_user_id_fkey')), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], name=op.f('vehicle_history_vehicle_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_history_pkey')), - schema='data' - ) - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc')) - ) - op.create_table('costs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('cost_type', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('mileage_at_cost', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('document_url', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('costs_user_id_fkey')), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], name=op.f('costs_vehicle_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('costs_pkey')), - schema='data' - ) - op.drop_index(op.f('ix_data_vehicle_ownerships_id'), table_name='vehicle_ownerships', schema='data') - op.drop_table('vehicle_ownerships', schema='data') - op.drop_index(op.f('ix_data_vehicle_assignments_id'), table_name='vehicle_assignments', schema='data') - op.drop_table('vehicle_assignments', schema='data') - op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data') - op.drop_table('company_members', schema='data') - op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data') - op.drop_table('companies', schema='data') - op.drop_index(op.f('ix_data_audit_logs_target_type'), table_name='audit_logs', schema='data') - op.drop_index(op.f('ix_data_audit_logs_target_id'), table_name='audit_logs', schema='data') - op.drop_index(op.f('ix_data_audit_logs_id'), table_name='audit_logs', schema='data') - op.drop_table('audit_logs', schema='data') - # ### end Alembic commands ### diff --git a/docker-compose.yml b/docker-compose.yml index 79dcbf9..1bb0ab4 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +version: '3.8' + services: # 1. MIGRÁCIÓ (Adatbázis szerkezet frissítése) migrate: @@ -7,9 +9,7 @@ services: container_name: service_finder_migrate env_file: .env volumes: - - ./backend:/app - - ./alembic.ini:/app/alembic.ini - - ./migrations:/app/migrations + - ./backend:/app # Ez tartalmazza az alembic.ini-t és a migrations mappát is! environment: PYTHONPATH: /app DATABASE_URL: ${MIGRATION_DATABASE_URL} @@ -28,10 +28,8 @@ services: env_file: .env volumes: - ./backend:/app - - ./alembic.ini:/app/alembic.ini - - ./migrations:/app/migrations - /mnt/nas/app_data:/mnt/nas/app_data # Központi NAS elérés - - ./static_previews:/app/static/previews # Lokális SSD gyorsítótár a miniképeknek + - ./static_previews:/app/static/previews # Lokális SSD gyorsítótár command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips="*" ports: - "8000:8000" @@ -72,7 +70,7 @@ services: - default restart: unless-stopped - # 4. REDIS (Lokális cache, NAS perzisztencia) + # 4. REDIS (Lokális cache) redis: image: redis:alpine container_name: service_finder_redis diff --git a/docs/V01_gemini/11_Gamification_Social.md b/docs/V01_gemini/11_Gamification_Social.md index b74e38c..93bd6fc 100644 --- a/docs/V01_gemini/11_Gamification_Social.md +++ b/docs/V01_gemini/11_Gamification_Social.md @@ -87,4 +87,18 @@ Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap, - **Célcsoport**: Kizárólag természetes személyek (`role: user`, `driver`). - **Kizárások**: Szervezetek (Organizations) és Adminisztrátorok nem gyűjtenek XP-t. - **Logika**: Minden `PointsLedger` bejegyzés kötelezően hivatkozik egy `user_id`-ra. -- **Mezőnevek**: Adatbázis szinten a pontok az `id`, `user_id`, `points`, `reason` mezőkben tárolódnak. \ No newline at end of file +- **Mezőnevek**: Adatbázis szinten a pontok az `id`, `user_id`, `points`, `reason` mezőkben tárolódnak. + +## 2026.02.10 FRISSÍTÉS - GAMIFICATION ÖKOSZISZTÉMA + +### 1. Pontrendszer Logika +A rendszer különválasztja a tekintélyt és a jutalmat: +- **XP (Tapasztalat):** Végleges szintlépéshez. Képlet: $BaseXP \times Level^{1.5}$. (Nehezedő görbe). +- **Social Points (Szezonális):** Időszakos versenyekhez (pl. Hónap Vadásza). +- **Kredit:** Fizetőeszköz, amit Social Pontokból lehet váltani (pl. 1000 pont = 100 Kredit). + +### 2. Konfiguráció +Minden érték (szorzók, határok) a \`GAMIFICATION_MASTER_CONFIG\` JSON paraméterben állítható Admin felületről, kódmódosítás nélkül. + +### 3. Audit +Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index 3732cd0..c03cedb 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -209,4 +209,44 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL - **SQLAlchemy hiba**: Javítva az `IndexError` a katalógus lekérdezésnél és az import hiba a `PG_UUID` kapcsán. ### Megjegyzés -A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasználót, létrehozni az egyéni flottáját, inicializálni a pénztárcáját (Kredit/Coin) és rögzíteni az első járművét kezdő km-óra állással. \ No newline at end of file +A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasználót, létrehozni az egyéni flottáját, inicializálni a pénztárcáját (Kredit/Coin) és rögzíteni az első járművét kezdő km-óra állással. + +## [Unreleased] - 2026-02-10 + +### 🚀 Added (Új funkciók) +- **RBAC System:** + - \`User\` tábla bővítése: \`scope_level\`, \`scope_id\`, \`custom_permissions\`. + - \`system_parameters\` tábla létrehozása a globális JSON konfigurációkhoz. + - Master RBAC JSON konfiguráció seedelése. +- **Gamification:** + - \`GamificationService\` létrehozása (XP, Level, Credit logika). + - Automata pontszámítás és nehezedő szintek logikája. +- **API Modularitás:** + - \`assets.py\` szétbontása 3 végpontra (Identity, Costs, Telemetry). + +### 🛠 Changed (Módosítások) +- **Asset Model:** Az \`Asset\` entitás mostantól lazy loading helyett \`selectinload\` stratégiát használ a teljesítmény érdekében. +- **Error Handling:** Javítottuk az \`asyncpg\` többszörös parancs-futtatási hibáját a setup scriptekben. +- **Configuration:** A rendszerbeállítások mostantól adatbázis-alapúak (JSONB) a hardcoded konstansok helyett. + +### 🐛 Fixed (Javítások) +- **Schema Mismatch:** SQLAlchemy modellek és Pydantic sémák szétválasztása (`models/asset.py` vs `schemas/asset.py`). +- **Data Integrity:** \`updated_at\` és \`is_active\` oszlopok pótlása a \`system_parameters\` táblában. +- **API Stability:** \`getattr\` használata a hiányzó opcionális mezőknél (pl. \`net_amount\`), hogy ne szálljon el az API 500-as hibával. + +## [1.2.0] - 2026-02-10 + +### Added +- **Asset Financials 2.0**: Pivot-Currency modell implementálva (helyi deviza + EUR párhuzamos tárolás). +- **Smart Auth Token**: JWT token mostantól tartalmazza a `rank`, `scope_level` és `scope_id` mezőket a gyors jogosultságkezeléshez. +- **CostService**: Automatikus árfolyam-kalkuláció, telemetria-szinkron és XP jóváírás költségrögzítéskor. +- **ExchangeRate**: Új árfolyamtábla és modell az EUR alapú váltásokhoz. + +### Fixed +- **Circular Import Resolution**: Megszüntetve a `db.base` és a `models` közötti körkörös függőség az import lánc modularizálásával. +- **Alembic Identity Sync**: Visszaállítva a `User` modell hiányzó `scope_level` és `custom_permissions` mezői, megakadályozva az adatvesztést migrációkor. +- **NotNullViolationError**: Fixálva az `asset_costs` tábla migrációja (amount_local NOT NULL kényszer). + +### Changed +- `AssetCost` modell mezőnevek szinkronizálva a pénzügyi standardokhoz (`amount_local`, `amount_eur`). +- `SystemParameter` modell elnevezés igazítva a meglévő adatbázis sémához. \ No newline at end of file diff --git a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md index 7495c58..7850f81 100644 --- a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md +++ b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md @@ -186,4 +186,19 @@ A járműadatok kezelése hibrid módon történik. ### 5.2 Költségkövetés (TCO) - Minden Asset-hez rögzíthető költség (`asset_costs`). - Kötelező adatok: Kategória, Összeg, Dátum. -- Opcionális: Km óra állása (az amortizáció és szervizintervallum számításához). \ No newline at end of file +- Opcionális: Km óra állása (az amortizáció és szervizintervallum számításához). + +## 2026.02.10 FRISSÍTÉS - ATOMIZÁLT ADATMODELL ÉS MODULÁRIS API + +### 1. Adatbázis Szerkezet (A 4 Pillér) +A járművek kezelése "Single Responsibility" elv alapján 4 modulra bomlott: +1. **Identity (Asset):** Alapadatok (VIN, Rendszám, Tulajdonos). +2. **Catalog (AssetCatalog):** Gyári statikus adatok (Típus, Motor, Akku). Ezt a Robotok töltik. +3. **Telemetry (AssetTelemetry):** Változó állapot (KM óra, VQI minőség index, DBS vezetési stílus). +4. **Financials (AssetCost):** Pénzügyi tranzakciók 9 kategóriába sorolva (Fuel, Service, Tax, stb.). + +### 2. Moduláris API Végpontok +A teljesítmény optimalizálása érdekében a \`Full Profile\` helyett 3 dedikált végpontot használunk: +- \`GET /api/v1/assets/{id}\`: Csak identitás és katalógus (Gyors nézet). +- \`GET /api/v1/assets/{id}/costs\`: Csak pénzügyi történet és grafikonok. +- \`GET /api/v1/assets/{id}/telemetry\`: Csak élő adatok (Dashboard). \ No newline at end of file diff --git a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md index af6d7df..029e2fe 100644 --- a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md +++ b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md @@ -86,4 +86,20 @@ A rendszer minden fontos paramétere a `data.system_settings` táblában tárolt ### 6.2 Paraméterezhető modulok * **Service Hunt:** Távolságok, XP/Kredit szorzók. * **Fraud Protection:** Strike limitek, kitiltási idők. -* **Billing:** EUR/HUF/USD váltószámok, csomagárak, jármű-slot árak. \ No newline at end of file +* **Billing:** EUR/HUF/USD váltószámok, csomagárak, jármű-slot árak. + +## 2026.02.10 FRISSÍTÉS - HIERARCHIKUS RBAC RENDSZER + +### 1. Rang-alapú Jogosultság (Rank System) +A rendszer a \`system_parameters\` táblában tárolt \`RBAC_MASTER_CONFIG\` JSON alapján működik. +- **SUPERADMIN (Rank 100):** Globális hatókör, mindent lát. +- **COUNTRY_ADMIN (Rank 80):** Országos felelős. +- **REGION_ADMIN (Rank 60):** Területi vezető (Manage Moderators). +- **MODERATOR (Rank 40):** Adatvalidátor. +- **SALES (Rank 20):** Üzletkötő (Csak saját partnerek). +- **USER (Rank 10):** Végfelhasználó. + +### 2. Scope (Hatókör) Védelem +Minden műveletnél ellenőrizzük a \`scope_id\` egyezését: +- Ha a felhasználó \`scope_level = 'region'\`, akkor csak olyan adatot szerkeszthet, ami ugyanahhoz a régióhoz tartozik. +- Kivétel: Impersonation (Megszemélyesítés) - Audit loggal védve. \ No newline at end of file diff --git a/docs/V01_gemini/_00_gemini_gem_kód b/docs/V01_gemini/_00_gemini_gem_kód index c224ea0..2b64f77 100644 --- a/docs/V01_gemini/_00_gemini_gem_kód +++ b/docs/V01_gemini/_00_gemini_gem_kód @@ -1,3 +1,135 @@ +🧬 SERVICE FINDER - UNIVERSAL SYSTEM PROMPT (v1.2) + +ROLE: Senior Technical Product Manager & Lead System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp SOURCE OF TRUTH: Grand Master Book (00–19) + 2026.02.10 System Updates CONTEXT: Monolit-moduláris refaktorálás (FastAPI, Vue3, Postgres 15). +1. VÍZIÓ ÉS KONTEXTUS (00, 01) + +Nem egy egyszerű nyilvántartó rendszert építünk, hanem egy Digital Twin (Digitális Iker) alapú ökoszisztémát. + + Core Philosophy: "A jármű örök, a tulajdonos vándor." (Vehicle-Centric Architecture). + + Pillére: + + Core Fleet: Életút és TCO követés. + + Marketplace: Szervizkereső és időpontfoglalás. + + Trust Engine: Bizonyíték alapú előélet (OCR, Fotó). + + Economy: Kredit és Gamification. + +2. TECHNOLÓGIAI STACK ÉS INFRA (02, 03, 04, 08) + + Frontend: Vue 3 (Composition API) + Vite + Tailwind CSS + Pinia. Dumb Frontend elv. + + Backend: Python 3.12 + FastAPI. Szigorú Pydantic validáció. + + Adatbázis: PostgreSQL 15. Két séma: data (üzleti), public (rendszer). + + Storage: MinIO (S3 kompatibilis) titkosított dokumentumokhoz. + + Hálózat: Internal Net (shared_db_net) zárt. Public Net: csak 80/443 (NPM Proxy). + + Config: Minden konfiguráció .env fájlból vagy data.system_parameters táblából jön. Hardkódolás TILOS. + +3. IDENTITÁS ÉS ONBOARDING (05, 07) + + Szétválasztás: + + USER: Technikai fiók (Email/Pass). + + PERSON: Valós jogi személy (Okmányok, KYC). Nem törölhető. + + Folyamat: Kétlépcsős (2-Step) Onboarding. + + Lite: Csak User létrehozása (is_active=False). + + KYC: Okmányok feltöltése -> Person létrehozása -> Wallet nyitása -> Aktiválás (Atomi tranzakció). + +4. ATOMIZÁLT ASSET MODELL (18) [FRISSÍTVE 2026.02.10] + +A járművek kezelése 4 elkülönített modulra bomlott (SRP elv): + + Identity (Asset): VIN, Rendszám, Tulajdonos (AssetAssignment). + + Catalog (AssetCatalog): Gyári statikus adatok. Robot Scout tölti. + + Telemetry (AssetTelemetry): Változó állapot (KM, VQI, DBS). + + Financials (AssetCost): Pénzügyi tranzakciók 9 kategóriában. + + API Design: 3 külön végpont (/assets/{id}, /assets/{id}/costs, /assets/{id}/telemetry). + +5. HIERARCHIKUS JOGOSULTSÁG (RBAC & SCOPE) (09, 19) [FRISSÍTVE 2026.02.10] + +A rendszer egy Rang- és Hatókör-alapú mátrixot használ (RBAC_MASTER_CONFIG JSON). + + Szintek (Rank): + + SUPERADMIN (100): Globális (L0). + + COUNTRY_ADMIN (80): Országos (L1). + + REGION_ADMIN (60): Területi (L1/B). + + MODERATOR (40): Adatvalidátor (L2). + + SALES (20): Üzletkötő (L3). + + USER (10): Végfelhasználó. + + Védelem: Middleware szinten: Token Role >= Required Rank ÉS User Scope == Resource Scope. + + Adattábla: User tábla új mezői: scope_level, scope_id, custom_permissions. + +6. GAMIFICATION ÉS ECONOMY (10, 11) [FRISSÍTVE 2026.02.10] + + XP (Tapasztalat): Végleges szintlépés (BaseXP×Level1.5). Nem csökken. + + Social Points: Szezonális, resetelhető pontok. + + Kredit: Valuta, Social pontokból váltható. + + Service: GamificationService és PointsLedger (auditált naplózás). + + Billing: Többvalutás rendszer (HUF/EUR tárolás). + +7. ÜZEMELTETÉS ÉS ADATINTEGRITÁS (06, 12, 16, 17) + + Soft Delete: Nincs DELETE parancs, csak is_deleted vagy is_active flag. + + Audit: Kritikus műveletek (pl. Impersonation) előtt/után állapotmentés az audit_logs táblába. + + Enum: Postgres Enum típusok mindig kisbetűsek (pl. role='user'). + + Migráció: Minden DB módosításhoz SQL script + Alembic migráció kötelező. + +🚀 KÖVETKEZŐ LÉPÉSEK (ACTION PLAN - 2026.02.11) + +A rendszer magja (Asset, RBAC, Gamification) stabil. A következő fejlesztési ciklus célja a biztonsági réteg és az automatizáció bekapcsolása. +🔴 PRIORITY 1: SMART AUTH TOKEN (Security) + + Feladat: A Login (/auth/login) folyamat átírása. + + Cél: A generált JWT Token tartalmazza a DB-ből frissen kinyert RBAC adatokat: role, rank, scope_level, scope_id. + + Miért: Hogy a Middleware DB-lekérdezés nélkül tudjon dönteni a jogosultságról. + + File: backend/app/core/security.py, backend/app/api/v1/endpoints/auth.py. + +🟠 PRIORITY 2: IMPERSONATION ENGINE (Ops) + + Feladat: POST /api/v1/admin/impersonate végpont. + + Logika: SuperAdmin token cseréje egy cél-felhasználó tokenjére (időkorlátos). + + Biztonság: Szigorú naplózás az audit_logs táblába (reason kötelező). + +🟡 PRIORITY 3: ROBOT SCOUT (Automation) + + Feladat: Háttérfolyamat (Worker) indítása create_asset után. + + Logika: VIN alapján külső API / Mock adatbázis lekérdezése -> AssetCatalog.factory_data feltöltése. + # 📘 SERVICE FINDER - MASTER ARCHITECT SYSTEM INSTRUCTIONS ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp 2.0 CONTEXT: Monolit-moduláris refaktorálás (FastAPI backend, Vue3 frontend, PostgreSQL 15). SSoT: Grand Master Book (v1.4). diff --git a/migrations/README b/migrations/README deleted file mode 100755 index 98e4f9c..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/migrations/__pycache__/env.cpython-312.pyc b/migrations/__pycache__/env.cpython-312.pyc deleted file mode 100755 index fa1c160f4da9b94547c9f54a7ec70a3f7084551a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3643 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 diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100755 index 777ef41..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,75 +0,0 @@ -import asyncio -from logging.config import fileConfig -import os -import sys - -from sqlalchemy import pool -from sqlalchemy.ext.asyncio import async_engine_from_config -from alembic import context - -# --- ÚTVONAL JAVÍTÁS --- -# Az aktuális fájl (env.py) helyéből kiindulva meghatározzuk a könyvtárakat -current_dir = os.path.dirname(os.path.realpath(__file__)) -project_root = os.path.realpath(os.path.join(current_dir, '..')) -backend_dir = os.path.join(project_root, 'backend') - -# Mindkét útvonalat betesszük a keresőbe, hogy a 'backend.app' és a sima 'app' is működjön -sys.path.insert(0, project_root) -sys.path.insert(0, backend_dir) - -# Most már az Alembic megtalálja a konfigurációt és a modelleket -try: - from app.core.config import settings - from app.db.base import Base - from app.models import * # Fontos, hogy minden modell be legyen importálva! -except ImportError as e: - print(f"Hiba az importálásnál: {e}") - print(f"Próbált útvonalak: {sys.path}") - raise - -config = context.config - -# Dinamikus adatbázis URL a .env alapján (App User jelszavával) -config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) - -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -target_metadata = Base.metadata - -def do_run_migrations(connection): - context.configure( - connection=connection, - target_metadata=target_metadata, - include_schemas=True, # Adatbázis sémák (pl. 'data') támogatása - version_table_schema='public' # Alembic tábla a public-ban marad - ) - - with context.begin_transaction(): - context.run_migrations() - -async def run_migrations_online() -> None: - """Aszinkron kapcsolat felépítése és migráció futtatása""" - connectable = async_engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - async with connectable.connect() as connection: - await connection.run_sync(do_run_migrations) - - await connectable.dispose() - -if context.is_offline_mode(): - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - include_schemas=True - ) - with context.begin_transaction(): - context.run_migrations() -else: - asyncio.run(run_migrations_online()) \ No newline at end of file diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100755 index 1101630..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,28 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - """Upgrade schema.""" - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - """Downgrade schema.""" - ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py b/migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py deleted file mode 100755 index 60bc499..0000000 --- a/migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py +++ /dev/null @@ -1,38 +0,0 @@ -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -revision = "10b73fee8967" -down_revision = "13d050e8cf6d" -branch_labels = None -depends_on = None - -def upgrade() -> None: - # 1. Régi táblák eltávolítása kényszerítve - tables_to_drop = [ - "user_badges", "badges", "points_ledger", "votes", "user_scores", - "user_stats", "competitions", "level_configs", "point_rules", - "regional_settings", "translations", "system_settings", "vehicles" - ] - for table in tables_to_drop: - op.execute(f"DROP TABLE IF EXISTS data.{table} CASCADE") - - # 2. Szerepkör oszlop típusának módosítása - op.execute("ALTER TABLE data.organization_members ALTER COLUMN role TYPE orguserrole USING role::text::orguserrole") - - # 3. Új oszlopok hozzáadása (Szigorúan Sima Idézőjellel a DEFAULT-nál!) - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS hashed_password VARCHAR") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS role VARCHAR DEFAULT \u0027user\u0027") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS is_superuser BOOLEAN DEFAULT FALSE") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS is_company BOOLEAN DEFAULT FALSE") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE") - - op.execute("ALTER TABLE data.organizations ADD COLUMN IF NOT EXISTS theme VARCHAR DEFAULT \u0027system\u0027") - op.execute("ALTER TABLE data.organizations ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE") - - # 4. Takarítás - op.execute("ALTER TABLE data.users DROP COLUMN IF EXISTS password_hash") - op.execute("ALTER TABLE data.users DROP COLUMN IF EXISTS is_email_verified") - -def downgrade() -> None: - pass diff --git a/migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py b/migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py deleted file mode 100755 index ff91dee..0000000 --- a/migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py +++ /dev/null @@ -1,607 +0,0 @@ -"""Add verification tokens and legal tables - -Revision ID: 13bd03551ebf -Revises: 8d450e9dc77f -Create Date: 2026-01-26 09:34:26.147993 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = '13bd03551ebf' -down_revision: Union[str, Sequence[str], None] = '8d450e9dc77f' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('email_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(), nullable=True), - sa.Column('type', sa.String(), nullable=True), - sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False, schema='data') - op.create_table('email_provider_configs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=True), - sa.Column('provider_type', sa.String(length=20), nullable=True), - sa.Column('priority', sa.Integer(), nullable=True), - sa.Column('settings', sa.JSON(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('fail_count', sa.Integer(), nullable=True), - sa.Column('max_fail_threshold', sa.Integer(), nullable=True), - sa.Column('success_rate', sa.Float(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name'), - schema='data' - ) - op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False, schema='data') - op.create_table('email_templates', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.Enum('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), nullable=True), - sa.Column('subject', sa.String(length=255), nullable=False), - sa.Column('body_html', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True, schema='data') - op.create_table('legal_documents', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=True), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('version', sa.String(length=20), nullable=False), - sa.Column('region_code', sa.String(length=5), nullable=True), - sa.Column('language', sa.String(length=5), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['id'], unique=False, schema='data') - op.create_table('legal_acceptances', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('document_id', sa.Integer(), nullable=True), - sa.Column('accepted_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['document_id'], ['data.legal_documents.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False, schema='data') - op.create_table('organizations', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('org_type', sa.Enum('PRIVATE', 'COMPANY', name='orgtype'), nullable=True), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('founded_at', sa.Date(), nullable=True), - sa.Column('validation_status', sa.Enum('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=True), - sa.Column('ui_theme', sa.String(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_organizations_id'), 'organizations', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=False, schema='data') - op.create_table('verification_tokens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('token', sa.String(length=255), nullable=True), - sa.Column('token_type', sa.Enum('EMAIL_VERIFY', 'PASSWORD_RESET', name='tokentype'), nullable=True), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True, schema='data') - op.create_table('organization_members', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('org_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', name='userrole'), nullable=True), - sa.Column('is_permanent', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False, schema='data') - op.create_table('vehicles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vin', sa.String(), nullable=False), - sa.Column('license_plate', sa.String(), nullable=False), - sa.Column('make', sa.String(), nullable=True), - sa.Column('model', sa.String(), nullable=True), - sa.Column('year', sa.Integer(), nullable=True), - sa.Column('is_deleted', sa.Boolean(), nullable=True), - sa.Column('current_org_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['current_org_id'], ['data.organizations.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicles_id'), 'vehicles', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_license_plate'), 'vehicles', ['license_plate'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_vin'), 'vehicles', ['vin'], unique=True, schema='data') - op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data') - op.drop_table('companies', schema='data') - op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data') - op.drop_table('company_members', schema='data') - op.drop_table('vehicle_options_catalog', schema='data') - op.drop_index(op.f('ix_data_vehicle_ownerships_id'), table_name='vehicle_ownerships', schema='data') - op.drop_table('vehicle_ownerships', schema='data') - op.drop_table('user_stats', schema='data') - op.drop_table('alembic_version') - op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules', schema='data') - op.drop_index(op.f('ix_data_point_rules_id'), table_name='point_rules', schema='data') - op.drop_table('point_rules', schema='data') - op.drop_index(op.f('ix_data_level_configs_id'), table_name='level_configs', schema='data') - op.drop_table('level_configs', schema='data') - op.drop_table('vehicle_engines', schema='data') - op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings', schema='data') - op.drop_table('system_settings', schema='data') - op.drop_table('vehicle_models', schema='data') - op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger', schema='data') - op.drop_index(op.f('ix_data_points_ledger_user_id'), table_name='points_ledger', schema='data') - op.drop_table('points_ledger', schema='data') - op.drop_index(op.f('ix_data_badges_id'), table_name='badges', schema='data') - op.drop_table('badges', schema='data') - op.drop_table('vehicle_categories', schema='data') - op.drop_table('competitions', schema='data') - op.drop_index(op.f('ix_data_service_providers_id'), table_name='service_providers', schema='data') - op.drop_table('service_providers', schema='data') - op.drop_table('vehicle_makes', schema='data') - op.drop_table('votes', schema='data') - op.drop_index(op.f('ix_data_vehicle_events_id'), table_name='vehicle_events', schema='data') - op.drop_table('vehicle_events', schema='data') - op.drop_index(op.f('ix_data_translations_id'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_key'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations', schema='data') - op.drop_table('translations', schema='data') - op.drop_table('staged_vehicle_data', schema='data') - op.drop_index(op.f('ix_data_locations_id'), table_name='locations', schema='data') - op.drop_table('locations', schema='data') - op.drop_index(op.f('ix_data_regional_settings_id'), table_name='regional_settings', schema='data') - op.drop_table('regional_settings', schema='data') - op.drop_table('user_badges', schema='data') - op.drop_index(op.f('ix_data_user_vehicles_id'), table_name='user_vehicles', schema='data') - op.drop_index(op.f('ix_data_user_vehicles_license_plate'), table_name='user_vehicles', schema='data') - op.drop_index(op.f('ix_data_user_vehicles_vin'), table_name='user_vehicles', schema='data') - op.drop_table('user_vehicles', schema='data') - op.drop_index(op.f('ix_data_audit_logs_id'), table_name='audit_logs', schema='data') - op.drop_index(op.f('ix_data_audit_logs_target_id'), table_name='audit_logs', schema='data') - op.drop_index(op.f('ix_data_audit_logs_target_type'), table_name='audit_logs', schema='data') - op.drop_table('audit_logs', schema='data') - op.drop_table('user_scores', schema='data') - op.drop_table('user_vehicle_modifications', schema='data') - op.drop_index(op.f('ix_data_vehicle_assignments_id'), table_name='vehicle_assignments', schema='data') - op.drop_table('vehicle_assignments', schema='data') - op.add_column('users', sa.Column('first_name', sa.String(), nullable=True), schema='data') - op.add_column('users', sa.Column('last_name', sa.String(), nullable=True), schema='data') - op.add_column('users', sa.Column('birthday', sa.Date(), nullable=True), schema='data') - op.add_column('users', sa.Column('is_email_verified', sa.Boolean(), nullable=True), schema='data') - op.add_column('users', sa.Column('is_banned', sa.Boolean(), nullable=True), schema='data') - op.add_column('users', sa.Column('is_gdpr_deleted', sa.Boolean(), nullable=True), schema='data') - op.add_column('users', sa.Column('previous_login_count', sa.Integer(), nullable=True), schema='data') - op.add_column('users', sa.Column('verified_at', sa.DateTime(timezone=True), nullable=True), schema='data') - op.drop_index(op.f('ix_data_users_region_code'), table_name='users', schema='data') - op.drop_column('users', 'is_superuser', schema='data') - op.drop_column('users', 'full_name', schema='data') - op.drop_column('users', 'reputation_score', schema='data') - op.drop_column('users', 'updated_at', schema='data') - op.drop_column('users', 'license_expiry_date', schema='data') - op.drop_column('users', 'role', schema='data') - op.drop_column('users', 'region_code', schema='data') - op.drop_column('users', 'id_card_expiry_date', schema='data') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('users', sa.Column('id_card_expiry_date', sa.DATE(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('region_code', sa.VARCHAR(length=5), server_default=sa.text("'HU'::character varying"), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('role', postgresql.ENUM('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='userrole'), server_default=sa.text("'USER'::userrole"), autoincrement=False, nullable=False), schema='data') - op.add_column('users', sa.Column('license_expiry_date', sa.DATE(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('reputation_score', sa.INTEGER(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('full_name', sa.VARCHAR(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('is_superuser', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='data') - op.create_index(op.f('ix_data_users_region_code'), 'users', ['region_code'], unique=False, schema='data') - op.drop_column('users', 'verified_at', schema='data') - op.drop_column('users', 'previous_login_count', schema='data') - op.drop_column('users', 'is_gdpr_deleted', schema='data') - op.drop_column('users', 'is_banned', schema='data') - op.drop_column('users', 'is_email_verified', schema='data') - op.drop_column('users', 'birthday', schema='data') - op.drop_column('users', 'last_name', schema='data') - op.drop_column('users', 'first_name', schema='data') - op.create_table('vehicle_assignments', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('company_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('driver_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('start_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('end_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('start_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('end_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('notes', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], name=op.f('vehicle_assignments_company_id_fkey')), - sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], name=op.f('vehicle_assignments_driver_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_assignments_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_assignments_id'), 'vehicle_assignments', ['id'], unique=False, schema='data') - op.create_table('user_vehicle_modifications', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('mod_type', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('is_validated', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('installed_at_km', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('cost', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicle_modifications_pkey')), - schema='data' - ) - op.create_table('user_scores', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('competition_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('last_updated', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['competition_id'], ['data.competitions.id'], name=op.f('user_scores_competition_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_scores_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_scores_pkey')), - sa.UniqueConstraint('user_id', 'competition_id', name=op.f('uq_user_competition_score'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_table('audit_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('target_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('target_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('action', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('user_agent', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('timestamp', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('audit_logs_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('audit_logs_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], unique=False, schema='data') - op.create_index(op.f('ix_data_audit_logs_target_id'), 'audit_logs', ['target_id'], unique=False, schema='data') - op.create_index(op.f('ix_data_audit_logs_id'), 'audit_logs', ['id'], unique=False, schema='data') - op.create_table('user_vehicles', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('nickname', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('make', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('model', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('year', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('fuel_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('engine_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('license_plate', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('vin', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('color_code', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('manufacture_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('registration_region', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('initial_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('current_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('mot_expiry_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('insurance_expiry_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('factory_options', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['engine_id'], ['data.vehicle_engines.id'], name=op.f('user_vehicles_engine_id_fkey')), - sa.ForeignKeyConstraint(['model_id'], ['data.vehicle_models.id'], name=op.f('user_vehicles_model_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_vehicles_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicles_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_user_vehicles_vin'), 'user_vehicles', ['vin'], unique=True, schema='data') - op.create_index(op.f('ix_data_user_vehicles_license_plate'), 'user_vehicles', ['license_plate'], unique=False, schema='data') - op.create_index(op.f('ix_data_user_vehicles_id'), 'user_vehicles', ['id'], unique=False, schema='data') - op.create_table('user_badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('badge_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('earned_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['badge_id'], ['data.badges.id'], name=op.f('user_badges_badge_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_badges_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_badges_pkey')), - schema='data' - ) - op.create_table('regional_settings', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('currency', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('regional_settings_pkey')), - sa.UniqueConstraint('country_code', name=op.f('regional_settings_country_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False, schema='data') - op.create_table('locations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('type', postgresql.ENUM('stop', 'warehouse', 'client', name='location_type_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('coordinates', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('address_full', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('capacity', sa.INTEGER(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('locations_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_locations_id'), 'locations', ['id'], unique=False, schema='data') - op.create_table('staged_vehicle_data', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('source_url', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('raw_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('status', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('error_log', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('staged_vehicle_data_pkey')), - schema='data' - ) - op.create_table('translations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('lang_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), - sa.Column('value', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('translations_pkey')), - sa.UniqueConstraint('key', 'lang_code', name=op.f('uq_translation_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False, schema='data') - op.create_table('vehicle_events', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('event_type', postgresql.ENUM('PURCHASE_PRICE', 'TRANSFER_TAX', 'ADMIN_FEE', 'VEHICLE_TAX', 'INSURANCE', 'REFUELING', 'SERVICE', 'PARKING', 'TOLL', 'FINE', 'TUNING_ACCESSORIES', 'OTHER', name='expense_category_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('odometer_value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('odometer_anomaly', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('cost_amount', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('image_paths', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('is_diy', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('service_provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['service_provider_id'], ['data.service_providers.id'], name=op.f('vehicle_events_service_provider_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_events_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_events_id'), 'vehicle_events', ['id'], unique=False, schema='data') - op.create_table('votes', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('vote_value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['provider_id'], ['data.service_providers.id'], name=op.f('votes_provider_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('votes_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('votes_pkey')), - sa.UniqueConstraint('user_id', 'provider_id', name=op.f('uq_user_provider_vote'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_table('vehicle_makes', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('logo_url', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_makes_pkey')), - sa.UniqueConstraint('name', name=op.f('vehicle_makes_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_table('service_providers', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('address', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('status', postgresql.ENUM('pending', 'approved', 'rejected', name='moderation_status_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('source', postgresql.ENUM('manual', 'ocr', 'api_import', name='source_type_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('validation_score', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('evidence_image_path', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('added_by_user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['added_by_user_id'], ['data.users.id'], name=op.f('service_providers_added_by_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('service_providers_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_service_providers_id'), 'service_providers', ['id'], unique=False, schema='data') - op.create_table('competitions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('start_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('end_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('competitions_pkey')), - schema='data' - ) - op.create_table('vehicle_categories', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name_key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_categories_pkey')), - schema='data' - ) - op.create_table('badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('icon_url', sa.VARCHAR(length=500), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('badges_pkey')), - sa.UniqueConstraint('name', name=op.f('badges_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False, schema='data') - op.create_table('points_ledger', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('points_change', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('reason', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('timestamp', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('points_ledger_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('points_ledger_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_points_ledger_user_id'), 'points_ledger', ['user_id'], unique=False, schema='data') - op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False, schema='data') - op.create_table('vehicle_models', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('make_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('generation_name', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('production_start_year', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('production_end_year', sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['category_id'], ['data.vehicle_categories.id'], name=op.f('vehicle_models_category_id_fkey')), - sa.ForeignKeyConstraint(['make_id'], ['data.vehicle_makes.id'], name=op.f('vehicle_models_make_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_models_pkey')), - schema='data' - ) - op.create_table('system_settings', - sa.Column('key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('value', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('key', name=op.f('system_settings_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key'], unique=False, schema='data') - op.create_table('vehicle_engines', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('engine_code', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('fuel_type', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('displacement_ccm', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('power_kw', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('torque_nm', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('transmission_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('gears_count', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('drive_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['model_id'], ['data.vehicle_models.id'], name=op.f('vehicle_engines_model_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_engines_pkey')), - schema='data' - ) - op.create_table('level_configs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('rank_name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('level_configs_pkey')), - sa.UniqueConstraint('level_number', name=op.f('level_configs_level_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_level_configs_id'), 'level_configs', ['id'], unique=False, schema='data') - op.create_table('point_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('action_key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('point_rules_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True, schema='data') - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc')) - ) - op.create_table('user_stats', - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('total_points', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False), - sa.Column('current_level', sa.INTEGER(), server_default=sa.text('1'), autoincrement=False, nullable=False), - sa.Column('last_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_stats_user_id_fkey')), - sa.PrimaryKeyConstraint('user_id', name=op.f('user_stats_pkey')), - schema='data' - ) - op.create_table('vehicle_ownerships', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('vehicle_ownerships_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_ownerships_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data') - op.create_table('vehicle_options_catalog', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('name_key', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_options_catalog_pkey')), - schema='data' - ) - op.create_table('company_members', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('company_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('role', postgresql.ENUM('OWNER', 'MANAGER', 'DRIVER', name='companyrole'), autoincrement=False, nullable=False), - sa.Column('can_edit_service', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('can_see_costs', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], name=op.f('company_members_company_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('company_members_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('company_members_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') - op.create_table('companies', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('tax_number', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('subscription_tier', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('owner_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], name=op.f('companies_owner_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('companies_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') - op.drop_index(op.f('ix_data_vehicles_vin'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_license_plate'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_id'), table_name='vehicles', schema='data') - op.drop_table('vehicles', schema='data') - op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members', schema='data') - op.drop_table('organization_members', schema='data') - op.drop_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens', schema='data') - op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens', schema='data') - op.drop_table('verification_tokens', schema='data') - op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations', schema='data') - op.drop_index(op.f('ix_data_organizations_id'), table_name='organizations', schema='data') - op.drop_table('organizations', schema='data') - op.drop_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances', schema='data') - op.drop_table('legal_acceptances', schema='data') - op.drop_index(op.f('ix_data_legal_documents_id'), table_name='legal_documents', schema='data') - op.drop_table('legal_documents', schema='data') - op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates', schema='data') - op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates', schema='data') - op.drop_table('email_templates', schema='data') - op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs', schema='data') - op.drop_table('email_provider_configs', schema='data') - op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs', schema='data') - op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs', schema='data') - op.drop_table('email_logs', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/13d050e8cf6d_initial_baseline_v2.py b/migrations/versions/13d050e8cf6d_initial_baseline_v2.py deleted file mode 100755 index 9aba351..0000000 --- a/migrations/versions/13d050e8cf6d_initial_baseline_v2.py +++ /dev/null @@ -1,200 +0,0 @@ -"""Initial baseline v2 - -Revision ID: 13d050e8cf6d -Revises: 13bd03551ebf -Create Date: 2026-01-26 09:53:50.248698 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '13d050e8cf6d' -down_revision: Union[str, Sequence[str], None] = '13bd03551ebf' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('email_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(), nullable=True), - sa.Column('type', sa.String(), nullable=True), - sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False, schema='data') - op.create_table('email_provider_configs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=True), - sa.Column('provider_type', sa.String(length=20), nullable=True), - sa.Column('priority', sa.Integer(), nullable=True), - sa.Column('settings', sa.JSON(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('fail_count', sa.Integer(), nullable=True), - sa.Column('max_fail_threshold', sa.Integer(), nullable=True), - sa.Column('success_rate', sa.Float(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name'), - schema='data' - ) - op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False, schema='data') - op.create_table('email_templates', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.Enum('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), nullable=True), - sa.Column('subject', sa.String(length=255), nullable=False), - sa.Column('body_html', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True, schema='data') - op.create_table('legal_documents', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=True), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('version', sa.String(length=20), nullable=False), - sa.Column('region_code', sa.String(length=5), nullable=True), - sa.Column('language', sa.String(length=5), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['id'], unique=False, schema='data') - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('password_hash', sa.Text(), nullable=False), - sa.Column('first_name', sa.String(), nullable=True), - sa.Column('last_name', sa.String(), nullable=True), - sa.Column('birthday', sa.Date(), nullable=True), - sa.Column('is_email_verified', sa.Boolean(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('is_banned', sa.Boolean(), nullable=True), - sa.Column('is_gdpr_deleted', sa.Boolean(), nullable=True), - sa.Column('previous_login_count', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('verified_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_users_email'), 'users', ['email'], unique=True, schema='data') - op.create_index(op.f('ix_data_users_id'), 'users', ['id'], unique=False, schema='data') - op.create_table('legal_acceptances', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('document_id', sa.Integer(), nullable=True), - sa.Column('accepted_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['document_id'], ['data.legal_documents.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False, schema='data') - op.create_table('organizations', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('org_type', sa.Enum('PRIVATE', 'COMPANY', name='orgtype'), nullable=True), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('founded_at', sa.Date(), nullable=True), - sa.Column('validation_status', sa.Enum('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=True), - sa.Column('ui_theme', sa.String(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_organizations_id'), 'organizations', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=False, schema='data') - op.create_table('verification_tokens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('token', sa.String(length=255), nullable=True), - sa.Column('token_type', sa.Enum('EMAIL_VERIFY', 'PASSWORD_RESET', name='tokentype'), nullable=True), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True, schema='data') - op.create_table('organization_members', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('org_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', name='userrole'), nullable=True), - sa.Column('is_permanent', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False, schema='data') - op.create_table('vehicles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vin', sa.String(), nullable=False), - sa.Column('license_plate', sa.String(), nullable=False), - sa.Column('make', sa.String(), nullable=True), - sa.Column('model', sa.String(), nullable=True), - sa.Column('year', sa.Integer(), nullable=True), - sa.Column('is_deleted', sa.Boolean(), nullable=True), - sa.Column('current_org_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['current_org_id'], ['data.organizations.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicles_id'), 'vehicles', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_license_plate'), 'vehicles', ['license_plate'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_vin'), 'vehicles', ['vin'], unique=True, schema='data') - op.drop_table('alembic_version') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc')) - ) - op.drop_index(op.f('ix_data_vehicles_vin'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_license_plate'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_id'), table_name='vehicles', schema='data') - op.drop_table('vehicles', schema='data') - op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members', schema='data') - op.drop_table('organization_members', schema='data') - op.drop_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens', schema='data') - op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens', schema='data') - op.drop_table('verification_tokens', schema='data') - op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations', schema='data') - op.drop_index(op.f('ix_data_organizations_id'), table_name='organizations', schema='data') - op.drop_table('organizations', schema='data') - op.drop_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances', schema='data') - op.drop_table('legal_acceptances', schema='data') - op.drop_index(op.f('ix_data_users_id'), table_name='users', schema='data') - op.drop_index(op.f('ix_data_users_email'), table_name='users', schema='data') - op.drop_table('users', schema='data') - op.drop_index(op.f('ix_data_legal_documents_id'), table_name='legal_documents', schema='data') - op.drop_table('legal_documents', schema='data') - op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates', schema='data') - op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates', schema='data') - op.drop_table('email_templates', schema='data') - op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs', schema='data') - op.drop_table('email_provider_configs', schema='data') - op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs', schema='data') - op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs', schema='data') - op.drop_table('email_logs', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/553ef1388276_rebuild_schema_v2.py b/migrations/versions/553ef1388276_rebuild_schema_v2.py deleted file mode 100755 index 7455c01..0000000 --- a/migrations/versions/553ef1388276_rebuild_schema_v2.py +++ /dev/null @@ -1,263 +0,0 @@ -"""rebuild_schema_v2 - -Revision ID: 553ef1388276 -Revises: c21c2c7e70d4 -Create Date: 2026-01-25 11:32:10.692756 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = '553ef1388276' -down_revision: Union[str, Sequence[str], None] = 'c21c2c7e70d4' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('system_settings', - sa.Column('key', sa.String(), nullable=False), - sa.Column('value', sa.JSON(), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('key'), - schema='data' - ) - op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key'], unique=False, schema='data') - op.drop_table('costs', schema='data') - op.drop_table('point_rules', schema='data') - op.drop_table('regional_settings', schema='data') - op.drop_table('level_configs', schema='data') - op.drop_table('vehicle_history', schema='data') - op.alter_column('badges', 'description', - existing_type=sa.TEXT(), - type_=sa.String(length=255), - nullable=False, - schema='data') - op.alter_column('badges', 'icon_url', - existing_type=sa.VARCHAR(length=255), - type_=sa.String(length=500), - existing_nullable=True, - schema='data') - op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False, schema='data') - op.create_unique_constraint(None, 'badges', ['name'], schema='data') - op.drop_column('badges', 'created_at', schema='data') - op.drop_column('badges', 'criteria_json', schema='data') - op.alter_column('points_ledger', 'user_id', - existing_type=sa.INTEGER(), - nullable=False, - schema='data') - op.alter_column('points_ledger', 'timestamp', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=False, - existing_server_default=sa.text('now()'), - schema='data') - op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_points_ledger_user_id'), 'points_ledger', ['user_id'], unique=False, schema='data') - op.add_column('translations', sa.Column('is_published', sa.Boolean(), nullable=True), schema='data') - op.drop_constraint('translations_key_lang_code_key', 'translations', schema='data', type_='unique') - op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False, schema='data') - op.create_unique_constraint('uq_translation_key_lang', 'translations', ['key', 'lang_code'], schema='data') - op.alter_column('user_badges', 'user_id', - existing_type=sa.INTEGER(), - nullable=False, - schema='data') - op.alter_column('user_badges', 'badge_id', - existing_type=sa.INTEGER(), - nullable=False, - schema='data') - op.alter_column('user_badges', 'earned_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=False, - existing_server_default=sa.text('now()'), - schema='data') - op.drop_constraint('user_badges_user_id_badge_id_key', 'user_badges', schema='data', type_='unique') - op.alter_column('user_stats', 'total_points', - existing_type=sa.INTEGER(), - nullable=False, - existing_server_default=sa.text('0'), - schema='data') - op.alter_column('user_stats', 'current_level', - existing_type=sa.INTEGER(), - nullable=False, - existing_server_default=sa.text('1'), - schema='data') - op.alter_column('user_stats', 'last_updated', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=False, - existing_server_default=sa.text('now()'), - schema='data') - - # --- JAVÍTOTT RÉSZ KEZDETE (Role konverzió default kezeléssel) --- - # 1. Meglévő default eldobása - op.execute('ALTER TABLE data.users ALTER COLUMN role DROP DEFAULT') - - # 2. Típusmódosítás - op.alter_column('users', 'role', - existing_type=postgresql.ENUM('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='user_role', schema='data'), - type_=sa.Enum('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='userrole'), - nullable=False, - schema='data', - postgresql_using='role::text::userrole') - - # 3. Új default beállítása az új típussal - op.execute("ALTER TABLE data.users ALTER COLUMN role SET DEFAULT 'USER'::userrole") - # --- JAVÍTOTT RÉSZ VÉGE --- - - op.drop_index('idx_users_role', table_name='users', schema='data') - op.create_index(op.f('ix_data_users_region_code'), 'users', ['region_code'], unique=False, schema='data') - op.create_foreign_key(None, 'vehicle_assignments', 'user_vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_index('ix_data_vehicle_events_vehicle_id', table_name='vehicle_events', schema='data') - op.create_foreign_key(None, 'vehicle_events', 'user_vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicle_ownerships', 'user_vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicle_events', schema='data', type_='foreignkey') - op.create_index('ix_data_vehicle_events_vehicle_id', 'vehicle_events', ['vehicle_id'], unique=False, schema='data') - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.drop_index(op.f('ix_data_users_region_code'), table_name='users', schema='data') - op.create_index('idx_users_role', 'users', ['role'], unique=False, schema='data') - op.alter_column('users', 'role', - existing_type=sa.Enum('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='userrole'), - type_=postgresql.ENUM('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='user_role', schema='data'), - nullable=True, - existing_server_default=sa.text("'USER'::data.user_role"), - schema='data') - op.alter_column('user_stats', 'last_updated', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=True, - existing_server_default=sa.text('now()'), - schema='data') - op.alter_column('user_stats', 'current_level', - existing_type=sa.INTEGER(), - nullable=True, - existing_server_default=sa.text('1'), - schema='data') - op.alter_column('user_stats', 'total_points', - existing_type=sa.INTEGER(), - nullable=True, - existing_server_default=sa.text('0'), - schema='data') - op.create_unique_constraint('user_badges_user_id_badge_id_key', 'user_badges', ['user_id', 'badge_id'], schema='data') - op.alter_column('user_badges', 'earned_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=True, - existing_server_default=sa.text('now()'), - schema='data') - op.alter_column('user_badges', 'badge_id', - existing_type=sa.INTEGER(), - nullable=True, - schema='data') - op.alter_column('user_badges', 'user_id', - existing_type=sa.INTEGER(), - nullable=True, - schema='data') - op.drop_constraint('uq_translation_key_lang', 'translations', schema='data', type_='unique') - op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_key'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_id'), table_name='translations', schema='data') - op.create_unique_constraint('translations_key_lang_code_key', 'translations', ['key', 'lang_code'], schema='data') - op.drop_column('translations', 'is_published', schema='data') - op.drop_index(op.f('ix_data_points_ledger_user_id'), table_name='points_ledger', schema='data') - op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger', schema='data') - op.alter_column('points_ledger', 'timestamp', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=True, - existing_server_default=sa.text('now()'), - schema='data') - op.alter_column('points_ledger', 'user_id', - existing_type=sa.INTEGER(), - nullable=True, - schema='data') - op.add_column('badges', sa.Column('criteria_json', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), schema='data') - op.add_column('badges', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), schema='data') - op.drop_constraint(None, 'badges', schema='data', type_='unique') - op.drop_index(op.f('ix_data_badges_id'), table_name='badges', schema='data') - op.alter_column('badges', 'icon_url', - existing_type=sa.String(length=500), - type_=sa.VARCHAR(length=255), - existing_nullable=True, - schema='data') - op.alter_column('badges', 'description', - existing_type=sa.String(length=255), - type_=sa.TEXT(), - nullable=True, - schema='data') - op.create_table('vehicle_history', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.vehicle_history_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('start_mileage', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('end_mileage', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), sa.Computed('(end_date IS NULL)', persisted=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name='vehicle_history_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='vehicle_history_pkey'), - schema='data' - ) - op.create_table('level_configs', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.level_configs_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('name_translation_key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('region_code', sa.VARCHAR(length=5), server_default=sa.text("'GLOBAL'::character varying"), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='level_configs_pkey'), - sa.UniqueConstraint('level_number', name='level_configs_level_number_key'), - schema='data' - ) - op.create_table('regional_settings', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.regional_settings_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), - sa.Column('setting_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('value', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('start_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('end_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='regional_settings_pkey'), - schema='data' - ) - op.create_table('point_rules', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.point_rules_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('rule_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('start_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('end_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('region_code', sa.VARCHAR(length=5), server_default=sa.text("'GLOBAL'::character varying"), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='point_rules_pkey'), - schema='data' - ) - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name='alembic_version_pkc') - ) - op.create_table('costs', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.costs_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('cost_type', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('mileage_at_cost', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('document_url', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name='costs_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='costs_pkey'), - schema='data' - ) - op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings', schema='data') - op.drop_table('system_settings', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py b/migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py deleted file mode 100755 index 694df0e..0000000 --- a/migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py +++ /dev/null @@ -1,28 +0,0 @@ -"""add persons and owner_person_id - -Revision ID: 5aed26900f0b -Revises: 10b73fee8967 -Create Date: 2026-02-01 22:22:21.486469 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '5aed26900f0b' -down_revision: Union[str, Sequence[str], None] = '10b73fee8967' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - pass - - -def downgrade() -> None: - """Downgrade schema.""" - pass diff --git a/migrations/versions/8d450e9dc77f_add_vehicle_staging.py b/migrations/versions/8d450e9dc77f_add_vehicle_staging.py deleted file mode 100755 index 3ddd055..0000000 --- a/migrations/versions/8d450e9dc77f_add_vehicle_staging.py +++ /dev/null @@ -1,35 +0,0 @@ -"""add_vehicle_staging - -Revision ID: 8d450e9dc77f -Revises: 553ef1388276 -Create Date: 2026-01-25 12:08:48.056502 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '8d450e9dc77f' -down_revision: Union[str, Sequence[str], None] = '553ef1388276' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - # op.drop_table('alembic_version') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name='alembic_version_pkc') - ) - # ### end Alembic commands ### diff --git a/migrations/versions/__pycache__/10b73fee8967_fix_roles_and_universal_vehicles.cpython-312.pyc b/migrations/versions/__pycache__/10b73fee8967_fix_roles_and_universal_vehicles.cpython-312.pyc deleted file mode 100755 index 73d9b2bcef11183a3e25f36eb2a7ee01b0734ae1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2341 zcmbVNO>7%Q6rQ!$_GS~e4TVAqMSDS!kw}6n(h{M98*f^-jvZ{Lw3g6ny)(8Kt#`LG zV<%pH&k zdo%NMGASeYe*9_2oKGP1TcC`-$gtDTK<5F%2n!Z!3Kc;J`VlKqiGqw;Vl!5W!I)Ty z<5(quBNYk$87$%WCt^hgnE*N4iKJ%!2q;s6A936~2-$TQuT#SATK?RdE7eOEYlK{W zd-W1%7xBtlE9A0KTgApOkWj>-jl$nQ5Q3=c5Y;!reRL0H(1$N0bYI{wKKMQwtza|& z5=ZU{UqRGgN53E91oXuP5k19AoS>hAj7^PTH?i@-4TV{M0nbpU0C14ZV`p&#QqXa1bwG+n|? zZaOyelNMW>HyL$jSNd0Vf%9qBVVpE4W~ogY zrUAa*`Ao5}rIgcGbE=ZvP}JMmQn{pHo$E`Uq^ze)>*TNv(XHXEj-T4p3(n%RxE@TomPv=TGyQ!AS>CG+W zgKYVFs4A60UiEGrBR!h<|49sQkR}C#i^kitAN4WEyug zOdZ#c4fNpBSCqf9%ew0>H_bX79MffBV!>fwKKjsTHFH-BsuLD&W36Sw+9`vxK03ln zZpTlwTwuY3E&z4#Ua}vc>7qYH@m#!{@5|^p<+IphN&Zs)T;6+O`C;taorjIRv+q8V z-aCkb@lZms#HTktzOi?Drzh?n#>eXIo_Oc67$4YbJ+VGkjGlcagdHUCA>xLd}_{5=oZ{rrZGsF49~l68BnqvKf-kX>_lI} zCO{3&XfVGoQPMVpB>Bl;Ms1>2;lecTbsL3o1B{eoV6f?of1cA+cnpLw;L+&}X?G|Bm{+!q&w_l3h%L6`~G F_BUPa)IUb~^1}uAay_XhiA@?v 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 QlfP(4Ye-ApCo%B<14L+SOaK4? diff --git a/migrations/versions/__pycache__/13d050e8cf6d_initial_baseline_v2.cpython-312.pyc b/migrations/versions/__pycache__/13d050e8cf6d_initial_baseline_v2.cpython-312.pyc deleted file mode 100755 index 215700c825f06654092b165204f17366605de5bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18123 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 Du<(aS diff --git a/migrations/versions/__pycache__/5aed26900f0b_add_persons_and_owner_person_id.cpython-312.pyc b/migrations/versions/__pycache__/5aed26900f0b_add_persons_and_owner_person_id.cpython-312.pyc deleted file mode 100755 index 502ae1b34160d8e0356f9eef7772b9647d599058..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1009 zcmah|&rcIU6rTOjcDr<2P|;}ou?ZKeY1uANpdoSq6TNVdaM{ab+Zkk&{l#pHmc-bD z2ab3`ZyYu8Kk))`A>+lwc<@HV#Ke?Ap7 zges8BG=Ej(>bfFk;I))jox(YMe!v^&@OsJ{WaNeF6p29!FqgV&xwyUU5@N=bB~h4| zZb;1NNk|#)uV9bp`XjpKC0-Po50@L}j7y1KpSP@atJ!C0(lBdQb9QQ-(z*HitiHsk z+ot9+K*O{xyI!&Eid8dhyCI%hb$YHoU7y$WuW&b1mbtb{H#;NBuO@;PB9KlKeJTI(C>)1ueuL)vIfG7du zvy8SoELrf)Wn{)wA6iJf4$yrJLQAEZ{xPQ?Dy;E**?l4olS|w0K;b`8P9jT(x`p i&i&nq-KYD?%V+!K;KH55!rdcepJrt#|6K=2?fC_Z+XImR diff --git a/migrations/versions/__pycache__/8d450e9dc77f_add_vehicle_staging.cpython-312.pyc b/migrations/versions/__pycache__/8d450e9dc77f_add_vehicle_staging.cpython-312.pyc deleted file mode 100755 index aa3516ec5f5cf82e3cbd7928a9ee98484c327543..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1382 zcmZ`&&2Jk;6rcU{dgHZQNI?*wmZAy_ieo!8*qvPm z90@6rkeXvSrInD#u_!t57jWm|D3OQ;ZnTHqDpG{Ri8pJTx>5V2o%d#b^X7g1_E*a? z5y)r8y8l2y=ufd3h}t(!55U+(O9&$yVF}BD6w0<7Dz+kty%MOQW^0M92D+WXnr)CY zF|j@io;YQX5C!%!Nxvrc>WU&X;6{S0cIE|mx{q5gz|91=@W^+noy8W;fX;Q)LUx0? z7(44^&G!Q0M6tW-(^b=Kkaa)un>4>LU(eU@`#w0%vOFdsHRi-CO<%vo)U#`?kwff~+u~eO|mMW(C2*v8U!{NAvn_pAsf6!WHF7?)&z+EOm#7#_EgyP6)Qcr{%78o5m z74zCf9jPF1EGE&x7C^qeYg@slF{TkVWKV3G{CI%7MDykAO9-9q9Xg0uT8mWFL6+G?sDdz}W9+v@h}nkx(aHFgMQ6!$h9x zB+t143735j*p)PmkMs?Pw!=qx@P(o40imn$+I66W<+kIdPd!FLLSt^wb^rqn2!Hv^ zLr&`}4_sC1hGemzGtmv60Y6}$6DKZmR%SG7**&@1zB_l0w);*uMc^^dT&p=!uTB)SVk!H@c&j{!sNh3tJ1@;SY@; z?R)lpcKl}LmrLEo7su+Cfc>V99jaq@Kk1gP9jn*>))Y-oLO_)|BXOJ2#lktBava?B z9Ea<1rv<+q6D?(87ChB2xmTMR-45J95VXU_#GRD5qd)~b=Qu8<&A7);$6OiYiB zh(%B@DdcRO#%$Qc?SOp1-i9lJ4p9e|$BHCLf62O}{*y)0_(Sx{@95RX20DLn)7TzA zLT{avuWVY|?VX!<-`ShGH*tTodtu@*S3E*jPRiBiBh`~)Wz*cgwllu-&7Si8w|ltz X(z}P5_l{8IRF|aJpJa8Z*Ps6Y0c}}y diff --git a/migrations/versions/__pycache__/c21c2c7e70d4_clean_gamification_setup.cpython-312.pyc b/migrations/versions/__pycache__/c21c2c7e70d4_clean_gamification_setup.cpython-312.pyc deleted file mode 100755 index 686d445d8dedaba26421b1eeca1c00c95b921803..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13066 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 AiU0rr diff --git a/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc b/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc deleted file mode 100644 index c5bfef7299f97e126bb174a03b6a4a2205159144..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104988 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 diff --git a/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py b/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py deleted file mode 100755 index 82ee655..0000000 --- a/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Clean gamification setup - -Revision ID: c21c2c7e70d4 -Revises: -Create Date: 2026-01-24 11:19:10.464212 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'c21c2c7e70d4' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('audit_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('target_type', sa.String(), nullable=True), - sa.Column('target_id', sa.Integer(), nullable=True), - sa.Column('action', sa.String(), nullable=False), - sa.Column('changes', sa.JSON(), nullable=True), - sa.Column('ip_address', sa.String(), nullable=True), - sa.Column('user_agent', sa.String(), nullable=True), - sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_audit_logs_id'), 'audit_logs', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_audit_logs_target_id'), 'audit_logs', ['target_id'], unique=False, schema='data') - op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], unique=False, schema='data') - op.create_table('companies', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('subscription_tier', sa.String(), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') - op.create_table('company_members', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('role', sa.Enum('OWNER', 'MANAGER', 'DRIVER', name='companyrole'), nullable=False), - sa.Column('can_edit_service', sa.Boolean(), nullable=True), - sa.Column('can_see_costs', sa.Boolean(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') - op.create_table('vehicle_assignments', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('company_id', sa.Integer(), nullable=False), - sa.Column('vehicle_id', sa.Integer(), nullable=False), - sa.Column('driver_id', sa.Integer(), nullable=False), - sa.Column('start_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('start_odometer', sa.Integer(), nullable=True), - sa.Column('end_odometer', sa.Integer(), nullable=True), - sa.Column('notes', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], ), - sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], ), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_assignments_id'), 'vehicle_assignments', ['id'], unique=False, schema='data') - op.create_table('vehicle_ownerships', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vehicle_id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('start_date', sa.Date(), nullable=False), - sa.Column('end_date', sa.Date(), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data') - # op.drop_table('costs', schema='data') - # op.drop_table('alembic_version') - # op.drop_table('vehicle_history', schema='data') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('vehicle_history', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('start_mileage', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('end_mileage', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), sa.Computed('(end_date IS NULL)', persisted=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('vehicle_history_user_id_fkey')), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], name=op.f('vehicle_history_vehicle_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_history_pkey')), - schema='data' - ) - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc')) - ) - op.create_table('costs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('cost_type', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('mileage_at_cost', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('document_url', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('costs_user_id_fkey')), - sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], name=op.f('costs_vehicle_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('costs_pkey')), - schema='data' - ) - op.drop_index(op.f('ix_data_vehicle_ownerships_id'), table_name='vehicle_ownerships', schema='data') - op.drop_table('vehicle_ownerships', schema='data') - op.drop_index(op.f('ix_data_vehicle_assignments_id'), table_name='vehicle_assignments', schema='data') - op.drop_table('vehicle_assignments', schema='data') - op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data') - op.drop_table('company_members', schema='data') - op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data') - op.drop_table('companies', schema='data') - op.drop_index(op.f('ix_data_audit_logs_target_type'), table_name='audit_logs', schema='data') - op.drop_index(op.f('ix_data_audit_logs_target_id'), table_name='audit_logs', schema='data') - op.drop_index(op.f('ix_data_audit_logs_id'), table_name='audit_logs', schema='data') - op.drop_table('audit_logs', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/fba92ed020b1_merge_identity_v1.py b/migrations/versions/fba92ed020b1_merge_identity_v1.py deleted file mode 100644 index af8356f..0000000 --- a/migrations/versions/fba92ed020b1_merge_identity_v1.py +++ /dev/null @@ -1,945 +0,0 @@ -"""merge_identity_v1 - -Revision ID: fba92ed020b1 -Revises: 5aed26900f0b -Create Date: 2026-02-04 21:31:43.854642 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'fba92ed020b1' -down_revision: Union[str, Sequence[str], None] = '5aed26900f0b' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user_vehicle_equipment') - op.drop_table('credit_logs') - op.drop_table('votes') - op.drop_table('audit_logs') - op.drop_index(op.f('ix_data_level_configs_id'), table_name='level_configs') - op.drop_table('level_configs') - op.drop_table('vouchers') - op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens') - op.drop_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens') - op.drop_index(op.f('ix_verification_tokens_lookup'), table_name='verification_tokens') - op.drop_index(op.f('ix_verification_tokens_user'), table_name='verification_tokens') - op.drop_index(op.f('uq_verification_tokens_token_hash'), table_name='verification_tokens', postgresql_where='(token_hash IS NOT NULL)') - op.drop_table('verification_tokens') - op.drop_index(op.f('ix_data_regional_settings_id'), table_name='regional_settings') - op.drop_table('regional_settings') - op.drop_index(op.f('ix_data_vehicle_ownership_id'), table_name='vehicle_ownership') - op.drop_table('vehicle_ownership') - op.drop_table('user_scores') - op.drop_index(op.f('idx_vm_slug'), table_name='vehicle_models') - op.drop_index(op.f('ix_data_vehicle_models_id'), table_name='vehicle_models') - op.drop_table('vehicle_models') - op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates') - op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates') - op.drop_table('email_templates') - op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger') - op.drop_table('points_ledger') - op.drop_table('bot_discovery_logs') - op.drop_table('equipment_items') - op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members') - op.drop_table('organization_members') - op.drop_index(op.f('idx_settings_lookup'), table_name='system_settings') - op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings') - op.drop_table('system_settings') - op.drop_table('user_credits') - op.drop_table('referrals') - op.drop_index(op.f('ix_data_vehicle_variants_id'), table_name='vehicle_variants') - op.drop_table('vehicle_variants') - op.drop_table('subscription_notification_rules') - op.drop_index(op.f('ix_data_badges_id'), table_name='badges') - op.drop_table('badges') - op.drop_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances') - op.drop_table('legal_acceptances') - op.drop_table('service_specialties') - op.drop_table('competitions') - op.drop_table('credit_transactions') - op.drop_table('locations') - op.drop_index(op.f('ix_data_legal_documents_id'), table_name='legal_documents') - op.drop_table('legal_documents') - op.drop_table('email_providers') - op.drop_table('subscription_tiers') - op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs') - op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs') - op.drop_table('email_logs') - op.drop_table('organization_locations') - op.drop_table('vehicle_events') - op.drop_table('vehicle_expenses') - op.drop_table('credit_rules') - op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs') - op.drop_table('email_provider_configs') - op.drop_table('org_subscriptions') - op.drop_index(op.f('ix_data_user_badges_id'), table_name='user_badges') - op.drop_table('user_badges') - op.drop_index(op.f('idx_vc_slug'), table_name='vehicle_categories') - op.drop_index(op.f('ix_data_vehicle_categories_id'), table_name='vehicle_categories') - op.drop_table('vehicle_categories') - op.drop_index(op.f('ix_data_user_vehicles_id'), table_name='user_vehicles') - op.drop_index(op.f('ix_data_user_vehicles_license_plate'), table_name='user_vehicles') - op.drop_index(op.f('ix_data_user_vehicles_vin'), table_name='user_vehicles') - op.drop_table('user_vehicles') - op.drop_table('fuel_stations') - op.drop_table('alembic_version') - op.drop_index(op.f('ix_data_translations_id'), table_name='translations') - op.drop_index(op.f('ix_data_translations_key'), table_name='translations') - op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations') - op.drop_table('translations') - op.drop_table('service_reviews') - op.drop_index(op.f('ix_data_user_stats_id'), table_name='user_stats') - op.drop_table('user_stats') - op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules') - op.drop_index(op.f('ix_data_point_rules_id'), table_name='point_rules') - op.drop_table('point_rules') - op.drop_index(op.f('ix_companies_owner_person_id'), table_name='companies') - op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') - op.drop_constraint(op.f('fk_companies_owner_person'), 'companies', type_='foreignkey') - op.drop_constraint(op.f('companies_owner_id_fkey'), 'companies', type_='foreignkey') - op.create_foreign_key(None, 'companies', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('companies', 'owner_person_id') - op.alter_column('company_members', 'role', - existing_type=sa.VARCHAR(length=50), - type_=postgresql.ENUM('owner', 'manager', 'driver', name='companyrole', schema='data'), - nullable=False, - existing_server_default=sa.text("'driver'::companyrole")) - op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') - op.drop_constraint(op.f('company_members_company_id_fkey'), 'company_members', type_='foreignkey') - op.drop_constraint(op.f('company_members_user_id_fkey'), 'company_members', type_='foreignkey') - op.create_foreign_key(None, 'company_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'company_members', 'companies', ['company_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_index(op.f('idx_engine_code'), table_name='engine_specs') - op.create_index(op.f('ix_data_engine_specs_id'), 'engine_specs', ['id'], unique=False, schema='data') - op.create_unique_constraint(None, 'engine_specs', ['engine_code'], schema='data') - op.drop_column('engine_specs', 'emissions_class') - op.drop_column('engine_specs', 'phases') - op.drop_column('engine_specs', 'default_service_interval_hours') - op.drop_column('engine_specs', 'onboard_charger_kw') - op.drop_column('engine_specs', 'battery_capacity_kwh') - op.drop_index(op.f('idx_org_slug'), table_name='organizations') - op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations') - op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey') - op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('organizations', 'theme') - op.drop_column('organizations', 'validation_status') - op.drop_column('organizations', 'founded_at') - op.drop_column('organizations', 'ui_theme') - op.drop_column('organizations', 'tax_number') - op.drop_column('organizations', 'slug') - op.drop_column('organizations', 'country_code') - op.add_column('persons', sa.Column('id_uuid', sa.UUID(), nullable=False)) - op.add_column('persons', sa.Column('last_name', sa.String(), nullable=False)) - op.add_column('persons', sa.Column('first_name', sa.String(), nullable=False)) - op.add_column('persons', sa.Column('mothers_name', sa.String(), nullable=True)) - op.add_column('persons', sa.Column('birth_place', sa.String(), nullable=True)) - op.add_column('persons', sa.Column('birth_date', sa.DateTime(), nullable=True)) - op.add_column('persons', sa.Column('identity_docs', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) - op.add_column('persons', sa.Column('medical_emergency', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) - op.add_column('persons', sa.Column('ice_contact', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) - op.alter_column('persons', 'id', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=False, - autoincrement=True) - op.create_index(op.f('ix_data_persons_id'), 'persons', ['id'], unique=False, schema='data') - op.create_unique_constraint(None, 'persons', ['id_uuid'], schema='data') - op.drop_column('persons', 'updated_at') - op.drop_column('persons', 'is_active') - op.drop_column('persons', 'reputation_score') - op.drop_column('persons', 'created_at') - op.drop_column('persons', 'risk_level') - op.alter_column('service_providers', 'search_tags', - existing_type=sa.TEXT(), - type_=sa.String(), - existing_nullable=True) - op.create_index(op.f('ix_data_service_providers_id'), 'service_providers', ['id'], unique=False, schema='data') - op.drop_column('service_providers', 'handled_vehicle_types') - op.drop_column('service_providers', 'verification_status') - op.drop_column('service_providers', 'specialized_brands') - op.drop_constraint(op.f('service_records_provider_id_fkey'), 'service_records', type_='foreignkey') - op.drop_constraint(op.f('service_records_vehicle_id_fkey'), 'service_records', type_='foreignkey') - op.create_foreign_key(None, 'service_records', 'service_providers', ['provider_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'service_records', 'vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('service_records', 'invoice_path') - op.drop_column('service_records', 'parts_quality_index') - op.drop_column('service_records', 'description') - op.drop_column('service_records', 'is_accident_repair') - op.drop_column('service_records', 'rating_impact_score') - op.alter_column('users', 'hashed_password', - existing_type=sa.VARCHAR(), - nullable=False) - op.alter_column('users', 'role', - existing_type=sa.VARCHAR(), - type_=sa.Enum('ADMIN', 'USER', 'SERVICE', 'FLEET_MANAGER', name='userrole'), - existing_nullable=True, - existing_server_default=sa.text("'user'::character varying")) - op.alter_column('users', 'is_deleted', - existing_type=sa.BOOLEAN(), - nullable=True, - existing_server_default=sa.text('false')) - op.alter_column('users', 'deleted_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=True) - op.alter_column('users', 'person_id', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=True) - op.drop_index(op.f('idx_user_email_active_only'), table_name='users', postgresql_where='((is_deleted IS FALSE) AND (deleted_at IS NULL))') - op.drop_index(op.f('ix_users_is_deleted'), table_name='users') - op.drop_index(op.f('ix_users_person_id'), table_name='users') - op.create_index(op.f('ix_data_users_email'), 'users', ['email'], unique=True, schema='data') - op.drop_constraint(op.f('fk_users_person'), 'users', type_='foreignkey') - op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('users', 'previous_login_count') - op.drop_column('users', 'first_name') - op.drop_column('users', 'is_gdpr_deleted') - op.drop_column('users', 'verified_at') - op.drop_column('users', 'is_banned') - op.drop_column('users', 'birthday') - op.drop_column('users', 'last_name') - op.alter_column('vehicle_assignments', 'vehicle_id', - existing_type=sa.INTEGER(), - type_=sa.UUID(), - existing_nullable=False) - op.drop_constraint(op.f('vehicle_assignments_company_id_fkey'), 'vehicle_assignments', type_='foreignkey') - op.drop_constraint(op.f('vehicle_assignments_vehicle_id_fkey'), 'vehicle_assignments', type_='foreignkey') - op.drop_constraint(op.f('vehicle_assignments_driver_id_fkey'), 'vehicle_assignments', type_='foreignkey') - op.create_foreign_key(None, 'vehicle_assignments', 'companies', ['company_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicle_assignments', 'vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicle_assignments', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_index(op.f('idx_vb_slug'), table_name='vehicle_brands') - op.drop_constraint(op.f('vehicle_brands_cat_name_key'), 'vehicle_brands', type_='unique') - op.create_unique_constraint(None, 'vehicle_brands', ['slug'], schema='data') - op.drop_constraint(op.f('vehicle_brands_category_id_fkey'), 'vehicle_brands', type_='foreignkey') - op.drop_column('vehicle_brands', 'country_code') - op.drop_column('vehicle_brands', 'category_id') - op.drop_column('vehicle_brands', 'origin_country') - op.drop_index(op.f('idx_vehicle_company'), table_name='vehicles') - op.drop_index(op.f('idx_vehicle_plate'), table_name='vehicles') - op.drop_index(op.f('idx_vehicle_vin'), table_name='vehicles') - op.drop_constraint(op.f('vehicles_engine_spec_id_fkey'), 'vehicles', type_='foreignkey') - op.drop_constraint(op.f('vehicles_current_company_id_fkey'), 'vehicles', type_='foreignkey') - op.drop_constraint(op.f('fk_vehicle_brand'), 'vehicles', type_='foreignkey') - op.create_foreign_key(None, 'vehicles', 'engine_specs', ['engine_spec_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicles', 'vehicle_brands', ['brand_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicles', 'companies', ['current_company_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('vehicles', 'custom_specs') - op.drop_column('vehicles', 'odometer_at_last_check') - op.drop_column('vehicles', 'factory_snapshot') - op.alter_column('wallets', 'id', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=False, - autoincrement=True) - op.alter_column('wallets', 'user_id', - existing_type=sa.INTEGER(), - nullable=True) - op.alter_column('wallets', 'xp_balance', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=True, - existing_server_default=sa.text('0')) - op.create_index(op.f('ix_data_wallets_id'), 'wallets', ['id'], unique=False, schema='data') - op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey') - op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('wallets', 'updated_at') - op.drop_column('wallets', 'created_at') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('wallets', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) - op.add_column('wallets', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id']) - op.drop_index(op.f('ix_data_wallets_id'), table_name='wallets', schema='data') - op.alter_column('wallets', 'xp_balance', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=True, - existing_server_default=sa.text('0')) - op.alter_column('wallets', 'user_id', - existing_type=sa.INTEGER(), - nullable=False) - op.alter_column('wallets', 'id', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=False, - autoincrement=True) - op.add_column('vehicles', sa.Column('factory_snapshot', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True)) - op.add_column('vehicles', sa.Column('odometer_at_last_check', sa.NUMERIC(precision=15, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=True)) - op.add_column('vehicles', sa.Column('custom_specs', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('fk_vehicle_brand'), 'vehicles', 'vehicle_brands', ['brand_id'], ['id']) - op.create_foreign_key(op.f('vehicles_current_company_id_fkey'), 'vehicles', 'companies', ['current_company_id'], ['id']) - op.create_foreign_key(op.f('vehicles_engine_spec_id_fkey'), 'vehicles', 'engine_specs', ['engine_spec_id'], ['id']) - op.create_index(op.f('idx_vehicle_vin'), 'vehicles', ['identification_number'], unique=False) - op.create_index(op.f('idx_vehicle_plate'), 'vehicles', ['license_plate'], unique=False) - op.create_index(op.f('idx_vehicle_company'), 'vehicles', ['current_company_id'], unique=False) - op.add_column('vehicle_brands', sa.Column('origin_country', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('vehicle_brands', sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True)) - op.add_column('vehicle_brands', sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.create_foreign_key(op.f('vehicle_brands_category_id_fkey'), 'vehicle_brands', 'vehicle_categories', ['category_id'], ['id']) - op.drop_constraint(None, 'vehicle_brands', schema='data', type_='unique') - op.create_unique_constraint(op.f('vehicle_brands_cat_name_key'), 'vehicle_brands', ['category_id', 'name'], postgresql_nulls_not_distinct=False) - op.create_index(op.f('idx_vb_slug'), 'vehicle_brands', ['slug'], unique=True) - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('vehicle_assignments_driver_id_fkey'), 'vehicle_assignments', 'users', ['driver_id'], ['id']) - op.create_foreign_key(op.f('vehicle_assignments_vehicle_id_fkey'), 'vehicle_assignments', 'user_vehicles', ['vehicle_id'], ['id']) - op.create_foreign_key(op.f('vehicle_assignments_company_id_fkey'), 'vehicle_assignments', 'companies', ['company_id'], ['id']) - op.alter_column('vehicle_assignments', 'vehicle_id', - existing_type=sa.UUID(), - type_=sa.INTEGER(), - existing_nullable=False) - op.add_column('users', sa.Column('last_name', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('birthday', sa.DATE(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('is_banned', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('verified_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('is_gdpr_deleted', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('first_name', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('previous_login_count', sa.INTEGER(), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'users', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('fk_users_person'), 'users', 'persons', ['person_id'], ['id']) - op.drop_index(op.f('ix_data_users_email'), table_name='users', schema='data') - op.create_index(op.f('ix_users_person_id'), 'users', ['person_id'], unique=False) - op.create_index(op.f('ix_users_is_deleted'), 'users', ['is_deleted', 'deleted_at'], unique=False) - op.create_index(op.f('idx_user_email_active_only'), 'users', ['email'], unique=True, postgresql_where='((is_deleted IS FALSE) AND (deleted_at IS NULL))') - op.alter_column('users', 'person_id', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=True) - op.alter_column('users', 'deleted_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=True) - op.alter_column('users', 'is_deleted', - existing_type=sa.BOOLEAN(), - nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('users', 'role', - existing_type=sa.Enum('ADMIN', 'USER', 'SERVICE', 'FLEET_MANAGER', name='userrole'), - type_=sa.VARCHAR(), - existing_nullable=True, - existing_server_default=sa.text("'user'::character varying")) - op.alter_column('users', 'hashed_password', - existing_type=sa.VARCHAR(), - nullable=True) - op.add_column('service_records', sa.Column('rating_impact_score', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('is_accident_repair', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('parts_quality_index', sa.NUMERIC(precision=3, scale=2), server_default=sa.text('1.0'), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('invoice_path', sa.TEXT(), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'service_records', schema='data', type_='foreignkey') - op.drop_constraint(None, 'service_records', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('service_records_vehicle_id_fkey'), 'service_records', 'vehicles', ['vehicle_id'], ['id']) - op.create_foreign_key(op.f('service_records_provider_id_fkey'), 'service_records', 'service_providers', ['provider_id'], ['id']) - op.add_column('service_providers', sa.Column('specialized_brands', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'[]'::jsonb"), autoincrement=False, nullable=True)) - op.add_column('service_providers', sa.Column('verification_status', sa.VARCHAR(length=20), server_default=sa.text("'pending'::character varying"), autoincrement=False, nullable=True)) - op.add_column('service_providers', sa.Column('handled_vehicle_types', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'["passenger_car"]\'::jsonb'), autoincrement=False, nullable=True)) - op.drop_index(op.f('ix_data_service_providers_id'), table_name='service_providers', schema='data') - op.alter_column('service_providers', 'search_tags', - existing_type=sa.String(), - type_=sa.TEXT(), - existing_nullable=True) - op.add_column('persons', sa.Column('risk_level', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('reputation_score', sa.NUMERIC(precision=10, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'persons', schema='data', type_='unique') - op.drop_index(op.f('ix_data_persons_id'), table_name='persons', schema='data') - op.alter_column('persons', 'id', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=False, - autoincrement=True) - op.drop_column('persons', 'ice_contact') - op.drop_column('persons', 'medical_emergency') - op.drop_column('persons', 'identity_docs') - op.drop_column('persons', 'birth_date') - op.drop_column('persons', 'birth_place') - op.drop_column('persons', 'mothers_name') - op.drop_column('persons', 'first_name') - op.drop_column('persons', 'last_name') - op.drop_column('persons', 'id_uuid') - op.add_column('organizations', sa.Column('country_code', sa.CHAR(length=2), server_default=sa.text("'HU'::bpchar"), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('slug', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('tax_number', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('ui_theme', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('founded_at', sa.DATE(), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('validation_status', postgresql.ENUM('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('theme', sa.VARCHAR(), server_default=sa.text("'system'::character varying"), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id']) - op.create_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=False) - op.create_index(op.f('idx_org_slug'), 'organizations', ['slug'], unique=True) - op.add_column('engine_specs', sa.Column('battery_capacity_kwh', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('onboard_charger_kw', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('default_service_interval_hours', sa.INTEGER(), server_default=sa.text('500'), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('phases', sa.INTEGER(), server_default=sa.text('3'), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('emissions_class', sa.VARCHAR(length=20), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'engine_specs', schema='data', type_='unique') - op.drop_index(op.f('ix_data_engine_specs_id'), table_name='engine_specs', schema='data') - op.create_index(op.f('idx_engine_code'), 'engine_specs', ['engine_code'], unique=False) - op.drop_constraint(None, 'company_members', schema='data', type_='foreignkey') - op.drop_constraint(None, 'company_members', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('company_members_user_id_fkey'), 'company_members', 'users', ['user_id'], ['id'], ondelete='CASCADE') - op.create_foreign_key(op.f('company_members_company_id_fkey'), 'company_members', 'companies', ['company_id'], ['id'], ondelete='CASCADE') - op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data') - op.alter_column('company_members', 'role', - existing_type=postgresql.ENUM('owner', 'manager', 'driver', name='companyrole', schema='data'), - type_=sa.VARCHAR(length=50), - nullable=True, - existing_server_default=sa.text("'driver'::companyrole")) - op.add_column('companies', sa.Column('owner_person_id', sa.BIGINT(), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'companies', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('companies_owner_id_fkey'), 'companies', 'users', ['owner_id'], ['id']) - op.create_foreign_key(op.f('fk_companies_owner_person'), 'companies', 'persons', ['owner_person_id'], ['id']) - op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data') - op.create_index(op.f('ix_companies_owner_person_id'), 'companies', ['owner_person_id'], unique=False) - op.create_table('point_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('action_key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('point_rules_pkey')) - ) - op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False) - op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True) - op.create_table('user_stats', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('total_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('current_level', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('last_activity', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_stats_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_stats_pkey')), - sa.UniqueConstraint('user_id', name=op.f('user_stats_user_id_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_user_stats_id'), 'user_stats', ['id'], unique=False) - op.create_table('service_reviews', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('service_record_id', sa.UUID(), autoincrement=False, nullable=True), - sa.Column('is_anonymous', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('overall_stars', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('detailed_ratings', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"comm": 0, "tech": 0, "clean": 0, "price": 0}\'::jsonb'), autoincrement=False, nullable=True), - sa.Column('comment', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.CheckConstraint('overall_stars >= 1 AND overall_stars <= 5', name=op.f('service_reviews_overall_stars_check')), - sa.ForeignKeyConstraint(['provider_id'], ['service_providers.id'], name=op.f('service_reviews_provider_id_fkey')), - sa.ForeignKeyConstraint(['service_record_id'], ['service_records.id'], name=op.f('service_reviews_service_record_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('service_reviews_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('service_reviews_pkey')), - sa.UniqueConstraint('user_id', 'provider_id', 'created_at', name=op.f('service_reviews_user_id_provider_id_created_at_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('translations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('lang_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), - sa.Column('value', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('lang', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('translations_pkey')), - sa.UniqueConstraint('key', 'lang_code', name=op.f('uq_translation_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False) - op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False) - op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False) - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc')) - ) - op.create_table('fuel_stations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('brand_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('location_city', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('latitude', sa.NUMERIC(precision=10, scale=8), autoincrement=False, nullable=True), - sa.Column('longitude', sa.NUMERIC(precision=11, scale=8), autoincrement=False, nullable=True), - sa.Column('amenities', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"food": false, "shop": false, "car_wash": "none"}\'::jsonb'), autoincrement=False, nullable=True), - sa.Column('fuel_types', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"diesel": true, "petrol_95": true, "ev_fast_charge": false}\'::jsonb'), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('fuel_stations_pkey')) - ) - op.create_table('user_vehicles', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vin', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('license_plate', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('variant_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('color', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('purchase_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.Column('purchase_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('current_odometer', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('extras', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('current_org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('is_deleted', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('tire_size_front', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('tire_size_rear', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('tire_dot_code', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.Column('custom_service_interval_km', sa.INTEGER(), server_default=sa.text('20000'), autoincrement=False, nullable=True), - sa.Column('last_service_km', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), - sa.Column('vin_verified', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('vin_deadline', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['current_org_id'], ['organizations.id'], name=op.f('user_vehicles_current_org_id_fkey')), - sa.ForeignKeyConstraint(['variant_id'], ['vehicle_variants.id'], name=op.f('user_vehicles_variant_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicles_pkey')) - ) - op.create_index(op.f('ix_data_user_vehicles_vin'), 'user_vehicles', ['vin'], unique=True) - op.create_index(op.f('ix_data_user_vehicles_license_plate'), 'user_vehicles', ['license_plate'], unique=False) - op.create_index(op.f('ix_data_user_vehicles_id'), 'user_vehicles', ['id'], unique=False) - op.create_table('vehicle_categories', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('slug', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_categories_pkey')), - sa.UniqueConstraint('name', name=op.f('vehicle_categories_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_vehicle_categories_id'), 'vehicle_categories', ['id'], unique=False) - op.create_index(op.f('idx_vc_slug'), 'vehicle_categories', ['slug'], unique=True) - op.create_table('user_badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('badge_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('earned_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['badge_id'], ['badges.id'], name=op.f('user_badges_badge_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_badges_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_badges_pkey')) - ) - op.create_index(op.f('ix_data_user_badges_id'), 'user_badges', ['id'], unique=False) - op.create_table('org_subscriptions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('tier_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('valid_from', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.Column('valid_until', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.Column('auto_renew', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.Column('trial_ends_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], name=op.f('org_subscriptions_org_id_fkey')), - sa.ForeignKeyConstraint(['tier_id'], ['subscription_tiers.id'], name=op.f('org_subscriptions_tier_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('org_subscriptions_pkey')) - ) - op.create_table('email_provider_configs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('provider_type', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('priority', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('settings', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('fail_count', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('max_fail_threshold', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('success_rate', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_provider_configs_pkey')), - sa.UniqueConstraint('name', name=op.f('email_provider_configs_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False) - op.create_table('credit_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('rule_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('label', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('credit_rules_pkey')), - sa.UniqueConstraint('rule_key', name=op.f('credit_rules_rule_key_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('vehicle_expenses', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('vehicle_id', sa.UUID(), autoincrement=False, nullable=False), - sa.Column('category', postgresql.ENUM('PURCHASE_PRICE', 'TRANSFER_TAX', 'ADMIN_FEE', 'VEHICLE_TAX', 'INSURANCE', 'REFUELING', 'SERVICE', 'PARKING', 'TOLL', 'FINE', 'TUNING_ACCESSORIES', 'OTHER', name='expense_category_enum'), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), server_default=sa.text('CURRENT_DATE'), autoincrement=False, nullable=True), - sa.Column('odometer_value', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['vehicle_id'], ['vehicles.id'], name=op.f('vehicle_expenses_vehicle_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_expenses_pkey')) - ) - op.create_table('vehicle_events', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('service_provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('event_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('odometer_reading', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('event_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['vehicle_id'], ['user_vehicles.id'], name=op.f('vehicle_events_vehicle_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_events_pkey')) - ) - op.create_table('organization_locations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('label', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('address', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('latitude', sa.NUMERIC(precision=10, scale=8), autoincrement=False, nullable=True), - sa.Column('longitude', sa.NUMERIC(precision=11, scale=8), autoincrement=False, nullable=True), - sa.Column('is_main_location', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], name=op.f('organization_locations_organization_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('organization_locations_pkey')) - ) - op.create_table('email_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('sent_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('recipient', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('status', sa.VARCHAR(length=50), server_default=sa.text("'sent'::character varying"), autoincrement=False, nullable=True), - sa.Column('email_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_logs_pkey')) - ) - op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False) - op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False) - op.create_table('subscription_tiers', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('rules', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('is_custom', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('subscription_tiers_pkey')), - sa.UniqueConstraint('name', name=op.f('subscription_tiers_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('email_providers', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('priority', sa.INTEGER(), server_default=sa.text('1'), autoincrement=False, nullable=True), - sa.Column('provider_type', sa.VARCHAR(length=10), server_default=sa.text("'SMTP'::character varying"), autoincrement=False, nullable=True), - sa.Column('host', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('port', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('username', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('password_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.Column('daily_limit', sa.INTEGER(), server_default=sa.text('300'), autoincrement=False, nullable=True), - sa.Column('current_daily_usage', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_providers_pkey')), - sa.UniqueConstraint('name', name=op.f('unique_provider_name'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('legal_documents', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('title', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('content', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('version', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('language', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('legal_documents_pkey')) - ) - op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['id'], unique=False) - op.create_table('locations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('address', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('latitude', sa.NUMERIC(precision=9, scale=6), autoincrement=False, nullable=True), - sa.Column('longitude', sa.NUMERIC(precision=9, scale=6), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('locations_pkey')) - ) - op.create_table('credit_transactions', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('reason', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('credit_transactions_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('credit_transactions_pkey')) - ) - op.create_table('competitions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('start_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('end_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('competitions_pkey')) - ) - op.create_table('service_specialties', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('parent_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('slug', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['parent_id'], ['service_specialties.id'], name=op.f('service_specialties_parent_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('service_specialties_pkey')), - sa.UniqueConstraint('slug', name=op.f('service_specialties_slug_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('legal_acceptances', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('document_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('accepted_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(length=45), autoincrement=False, nullable=True), - sa.Column('user_agent', sa.TEXT(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['document_id'], ['legal_documents.id'], name=op.f('legal_acceptances_document_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('legal_acceptances_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('legal_acceptances_pkey')) - ) - op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False) - op.create_table('badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('icon_url', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('badges_pkey')), - sa.UniqueConstraint('name', name=op.f('badges_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False) - op.create_table('subscription_notification_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('subscription_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('days_before', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('template_key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('subscription_notification_rules_pkey')) - ) - op.create_table('vehicle_variants', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('engine_size', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('power_kw', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('spec_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('fuel_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('engine_code', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('cylinder_capacity', sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['model_id'], ['vehicle_models.id'], name=op.f('vehicle_variants_model_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_variants_pkey')) - ) - op.create_index(op.f('ix_data_vehicle_variants_id'), 'vehicle_variants', ['id'], unique=False) - op.create_table('referrals', - sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), - sa.Column('referrer_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('referee_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('commission_level', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('commission_percentage', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.CheckConstraint('commission_level = ANY (ARRAY[1, 2, 3])', name=op.f('referrals_commission_level_check')), - sa.ForeignKeyConstraint(['referee_id'], ['users.id'], name=op.f('referrals_referee_id_fkey')), - sa.ForeignKeyConstraint(['referrer_id'], ['users.id'], name=op.f('referrals_referrer_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('referrals_pkey')) - ) - op.create_table('user_credits', - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('balance', sa.NUMERIC(precision=15, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=True), - sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_credits_user_id_fkey')), - sa.PrimaryKeyConstraint('user_id', name=op.f('user_credits_pkey')) - ) - op.create_table('system_settings', - sa.Column('key_name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('value_json', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('tier_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('value', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('key_name', name=op.f('system_settings_pkey')) - ) - op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key_name'], unique=False) - op.create_index(op.f('idx_settings_lookup'), 'system_settings', ['key_name', sa.literal_column("COALESCE(region_code, ''::character varying)"), sa.literal_column('COALESCE(tier_id, 0)'), sa.literal_column('COALESCE(org_id, 0)')], unique=True) - op.create_table('organization_members', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('role', postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'owner', 'manager', 'driver', 'service', name='orguserrole'), autoincrement=False, nullable=True), - sa.Column('is_permanent', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], name=op.f('organization_members_org_id_fkey'), ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('organization_members_user_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('organization_members_pkey')), - sa.UniqueConstraint('organization_id', 'user_id', name=op.f('unique_user_org'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False) - op.create_table('equipment_items', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('equipment_items_pkey')) - ) - op.create_table('bot_discovery_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('category', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('brand_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('model_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('action_taken', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('discovered_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('bot_discovery_logs_pkey')) - ) - op.create_table('points_ledger', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('reason', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('points_ledger_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('points_ledger_pkey')) - ) - op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False) - op.create_table('email_templates', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('type', postgresql.ENUM('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), autoincrement=False, nullable=True), - sa.Column('subject', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('body_html', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('lang', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_templates_pkey')), - sa.UniqueConstraint('key', 'lang', name=op.f('unique_email_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True) - op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False) - op.create_table('vehicle_models', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('brand_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('year_start', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('year_end', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('slug', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['brand_id'], ['vehicle_brands.id'], name=op.f('vehicle_models_brand_id_fkey')), - sa.ForeignKeyConstraint(['category_id'], ['vehicle_categories.id'], name=op.f('vehicle_models_category_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_models_pkey')), - sa.UniqueConstraint('brand_id', 'name', name=op.f('vehicle_models_brand_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_vehicle_models_id'), 'vehicle_models', ['id'], unique=False) - op.create_index(op.f('idx_vm_slug'), 'vehicle_models', ['brand_id', 'slug'], unique=True) - op.create_table('user_scores', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('competition_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('last_updated', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['competition_id'], ['competitions.id'], name=op.f('user_scores_competition_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_scores_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_scores_pkey')), - sa.UniqueConstraint('user_id', 'competition_id', name=op.f('uq_user_competition_score'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('vehicle_ownership', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('license_plate', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('start_date', sa.DATE(), server_default=sa.text('CURRENT_DATE'), autoincrement=False, nullable=True), - sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('vehicle_ownership_user_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_ownership_pkey')) - ) - op.create_index(op.f('ix_data_vehicle_ownership_id'), 'vehicle_ownership', ['id'], unique=False) - op.create_table('regional_settings', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('currency_code', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column('language_code', sa.CHAR(length=2), server_default=sa.text("'hu'::bpchar"), autoincrement=False, nullable=True), - sa.Column('is_eu_member', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('regional_settings_pkey')), - sa.UniqueConstraint('country_code', name=op.f('regional_settings_country_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False) - op.create_table('verification_tokens', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('token', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('token_type', postgresql.ENUM('email_verify', 'password_reset', 'api_key', name='tokentype'), autoincrement=False, nullable=True), - sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('token_hash', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('is_used', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('used_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('verification_tokens_user_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('verification_tokens_pkey')) - ) - op.create_index(op.f('uq_verification_tokens_token_hash'), 'verification_tokens', ['token_hash'], unique=True, postgresql_where='(token_hash IS NOT NULL)') - op.create_index(op.f('ix_verification_tokens_user'), 'verification_tokens', ['user_id', 'token_type', sa.literal_column('created_at DESC')], unique=False) - op.create_index(op.f('ix_verification_tokens_lookup'), 'verification_tokens', ['token_type', 'is_used', 'expires_at'], unique=False) - op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True) - op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False) - op.create_table('vouchers', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('code', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('value', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('batch_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('is_used', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('used_by', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('used_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['used_by'], ['users.id'], name=op.f('vouchers_used_by_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vouchers_pkey')), - sa.UniqueConstraint('code', name=op.f('vouchers_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('level_configs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('rank_name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('level_configs_pkey')), - sa.UniqueConstraint('level_number', name=op.f('level_configs_level_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_level_configs_id'), 'level_configs', ['id'], unique=False) - op.create_table('audit_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('action', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('endpoint', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('method', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(length=45), autoincrement=False, nullable=True), - sa.Column('user_agent', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('audit_logs_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('audit_logs_pkey')) - ) - op.create_table('votes', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('vote_value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('votes_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('votes_pkey')), - sa.UniqueConstraint('user_id', 'provider_id', name=op.f('uq_user_provider_vote'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('credit_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], name=op.f('credit_logs_org_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('credit_logs_pkey')) - ) - op.create_table('user_vehicle_equipment', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('equipment_item_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('source', postgresql.ENUM('factory', 'aftermarket', name='equipment_source'), server_default=sa.text("'factory'::equipment_source"), autoincrement=False, nullable=True), - sa.Column('installed_at', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['equipment_item_id'], ['equipment_items.id'], name=op.f('user_vehicle_equipment_equipment_item_id_fkey')), - sa.ForeignKeyConstraint(['user_vehicle_id'], ['user_vehicles.id'], name=op.f('user_vehicle_equipment_user_vehicle_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicle_equipment_pkey')) - ) - # ### end Alembic commands ###