From a57d5333d478638c3b4bad9862eb8888a920a890 Mon Sep 17 00:00:00 2001 From: Kincses Date: Wed, 4 Feb 2026 21:58:57 +0000 Subject: [PATCH] Save test environment changes --- .env | 63 +- .../old_other/CHANGELOG.md | 0 .../old_other/backup_20260128_alap_kesz.sql | 0 .../old_other/backup_to_nas.sh | 0 .../old_other/deploy_v16.sh | 0 .../old_other/docker-compose_2026.02.01.yml | 0 init_dev.sh => archive/old_other/init_dev.sh | 0 .../old_specs/api_spec.json | 0 .../old_specs/api_spec_v2.json | 0 backend/app/__pycache__/main.cpython-312.pyc | Bin 2237 -> 2307 bytes .../api/v1/__pycache__/api.cpython-312.pyc | Bin 1121 -> 402 bytes backend/app/api/v1/api.py | 15 +- .../__pycache__/auth.cpython-312.pyc | Bin 5984 -> 1964 bytes backend/app/api/v1/endpoints/auth.py | 105 +- .../core/__pycache__/config.cpython-312.pyc | Bin 1756 -> 3410 bytes .../core/__pycache__/security.cpython-312.pyc | Bin 2342 -> 2605 bytes backend/app/core/config.py | 75 +- backend/app/core/security.py | 26 +- backend/app/main.py | 37 +- backend/app/models/__init__.py | 11 +- .../__pycache__/__init__.cpython-312.pyc | Bin 697 -> 826 bytes .../__pycache__/identity.cpython-312.pyc | Bin 0 -> 3830 bytes .../__pycache__/organization.cpython-312.pyc | Bin 0 -> 1653 bytes backend/app/models/identity.py | 73 ++ backend/app/models/organization.py | 20 +- backend/app/models/user.py | 34 +- .../schemas/__pycache__/auth.cpython-312.pyc | Bin 0 -> 1811 bytes backend/app/schemas/auth.py | 29 +- .../__pycache__/auth_service.cpython-312.pyc | Bin 0 -> 4265 bytes backend/app/services/auth_service.py | 81 ++ .../1_PROJECT_BRAIN_FLEET.md | 0 .../2_MODULE_STATUS_FLEET.md | 0 .../3_IMPLEMENTED_FEATURES.md | 0 docs/{ => Old_versions}/4_BACKLOG_FLEET.md | 0 docs/{ => Old_versions}/5_TECH_DEBT_FLEET.md | 0 docs/{ => Old_versions}/6_ROADMAP_FLEET.md | 0 docs/{ => Old_versions}/AI üzemeltetése.md | 0 .../DB_STATE_FLEET_2026-01-28.md | 0 docs/{ => Old_versions}/Naplócsomag | 0 docs/{ => Old_versions}/Projekt értékelés.md | 0 .../Old_versions/Promptok gemekhez.txt | 0 ...inder Rendszerspecifikáció es feljesztes.txt | 0 .../Old_versions/_Adatbázis_állalot_napló.txt | 0 .../Old_versions/_Horgony_megjegyzések.txt | 0 .../Old_versions/_Projekt Állapot jelentés.txt | 0 .../Old_versions/_valtozok_konyve.txt | 0 lista.txt => docs/Old_versions/lista.txt | 0 mappak.txt => docs/Old_versions/mappak.txt | 0 .../Old_versions/projekt_terkep.txt | 0 docs/{ => Old_versions}/teljes_log | 0 docs/V01_chatgpt/00_README.md | 15 + docs/V01_chatgpt/01_Project_Overview.md | 24 + .../02_Architecture_System_Context.md | 0 .../V01_chatgpt/03_Dev_Environment_Runbook.md | 19 + docs/V01_chatgpt/06_Database_Guide.md | 26 + docs/V01_chatgpt/07_API_Guide.md | 13 + docs/V01_chatgpt/13_Roadmap_Tech_Debt.md | 0 docs/V01_chatgpt/14_Anchor_Log_Timeline.md | 10 + docs/V01_chatgpt/15_Changelog.md | 6 + docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md | 40 + docs/V01_gemini/05_Security_Model.md | 17 - docs/V01_gemini/06_Database_Guide.md | 34 +- .../07_REGISTRATION_INVITATION_AND_API.md | 52 + .../10_Billing_Credits_Subscriptions.md | 50 +- .../16_TESTING_AND_DEPLOYMENT_GUIDE.md | 22 + ...2ed020b1_merge_identity_v1.cpython-312.pyc | Bin 0 -> 104988 bytes .../fba92ed020b1_merge_identity_v1.py | 945 ++++++++++++++++++ 67 files changed, 1603 insertions(+), 239 deletions(-) rename CHANGELOG.md => archive/old_other/CHANGELOG.md (100%) rename backup_20260128_alap_kesz.sql => archive/old_other/backup_20260128_alap_kesz.sql (100%) rename backup_to_nas.sh => archive/old_other/backup_to_nas.sh (100%) rename deploy_v16.sh => archive/old_other/deploy_v16.sh (100%) rename docker-compose_2026.02.01.yml => archive/old_other/docker-compose_2026.02.01.yml (100%) rename init_dev.sh => archive/old_other/init_dev.sh (100%) rename api_spec.json => archive/old_specs/api_spec.json (100%) rename api_spec_v2.json => archive/old_specs/api_spec_v2.json (100%) mode change 100755 => 100644 backend/app/__pycache__/main.cpython-312.pyc mode change 100755 => 100644 backend/app/api/v1/__pycache__/api.cpython-312.pyc mode change 100755 => 100644 backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc mode change 100755 => 100644 backend/app/core/__pycache__/config.cpython-312.pyc mode change 100755 => 100644 backend/app/core/__pycache__/security.cpython-312.pyc mode change 100755 => 100644 backend/app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/identity.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/organization.cpython-312.pyc create mode 100644 backend/app/models/identity.py create mode 100644 backend/app/schemas/__pycache__/auth.cpython-312.pyc create mode 100644 backend/app/services/__pycache__/auth_service.cpython-312.pyc create mode 100644 backend/app/services/auth_service.py rename docs/{ => Old_versions}/1_PROJECT_BRAIN_FLEET.md (100%) rename docs/{ => Old_versions}/2_MODULE_STATUS_FLEET.md (100%) rename docs/{ => Old_versions}/3_IMPLEMENTED_FEATURES.md (100%) rename docs/{ => Old_versions}/4_BACKLOG_FLEET.md (100%) rename docs/{ => Old_versions}/5_TECH_DEBT_FLEET.md (100%) rename docs/{ => Old_versions}/6_ROADMAP_FLEET.md (100%) rename docs/{ => Old_versions}/AI üzemeltetése.md (100%) rename docs/{ => Old_versions}/DB_STATE_FLEET_2026-01-28.md (100%) rename docs/{ => Old_versions}/Naplócsomag (100%) rename docs/{ => Old_versions}/Projekt értékelés.md (100%) rename Promptok gemekhez.txt => docs/Old_versions/Promptok gemekhez.txt (100%) rename Service_finder Rendszerspecifikáció es feljesztes.txt => docs/Old_versions/Service_finder Rendszerspecifikáció es feljesztes.txt (100%) rename _Adatbázis_állalot_napló.txt => docs/Old_versions/_Adatbázis_állalot_napló.txt (100%) rename _Horgony_megjegyzések.txt => docs/Old_versions/_Horgony_megjegyzések.txt (100%) rename _Projekt Állapot jelentés.txt => docs/Old_versions/_Projekt Állapot jelentés.txt (100%) rename _valtozok_konyve.txt => docs/Old_versions/_valtozok_konyve.txt (100%) rename lista.txt => docs/Old_versions/lista.txt (100%) rename mappak.txt => docs/Old_versions/mappak.txt (100%) rename projekt_terkep.txt => docs/Old_versions/projekt_terkep.txt (100%) rename docs/{ => Old_versions}/teljes_log (100%) create mode 100644 docs/V01_chatgpt/00_README.md create mode 100644 docs/V01_chatgpt/01_Project_Overview.md create mode 100644 docs/V01_chatgpt/02_Architecture_System_Context.md create mode 100644 docs/V01_chatgpt/03_Dev_Environment_Runbook.md create mode 100644 docs/V01_chatgpt/06_Database_Guide.md create mode 100644 docs/V01_chatgpt/07_API_Guide.md create mode 100644 docs/V01_chatgpt/13_Roadmap_Tech_Debt.md create mode 100644 docs/V01_chatgpt/14_Anchor_Log_Timeline.md create mode 100644 docs/V01_chatgpt/15_Changelog.md create mode 100644 docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md delete mode 100644 docs/V01_gemini/05_Security_Model.md create mode 100644 docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md create mode 100644 docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md create mode 100644 migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc create mode 100644 migrations/versions/fba92ed020b1_merge_identity_v1.py diff --git a/.env b/.env index 467c423..6864797 100755 --- a/.env +++ b/.env @@ -29,4 +29,65 @@ SENDGRID_API_KEY=SG.SENDGRID_API_KEY=SG.XspCvW0ERPC_zdVI6AgjTw.85MHZyPYnHQbUoVDj FROM_EMAIL=info@profibot.hu # Biztonsági kulcs a tokenekhez (KÖTELEZŐ!) -SECRET_KEY=2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b \ No newline at end of file +SECRET_KEY=2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b +_______________________________________________________________ +# ============================================================================== +# 🛠️ INFRASTRUKTÚRA (Docker & Database) +# ============================================================================== +# Adatbázis alapok +POSTGRES_USER=kincses +POSTGRES_PASSWORD='MiskociA74' +POSTGRES_DB=service_finder + +# Kapcsolati URL a Python számára (Központi shared-postgres) +DATABASE_URL=postgresql+asyncpg://service_finder_app:MiskociA74@shared-postgres:5432/service_finder + +# Migrációhoz használt URL (Alembic számára) +MIGRATION_DATABASE_URL=postgresql+asyncpg://service_finder_app:MiskociA74@shared-postgres:5432/service_finder + +# Redis elérés +REDIS_URL=redis://service_finder_redis:6379/0 + +# ============================================================================== +# 🚀 ALKALMAZÁS BEÁLLÍTÁSOK (FastAPI) +# ============================================================================== +ENV=development +DEBUG=True +PYTHONPATH=/app + +# Biztonsági kulcs a JWT tokenekhez (Generálj egy hosszú véletlen sort!) +# Példa generáláshoz: openssl rand -hex 32 +SECRET_KEY='2dca2ff3bf9b8184e14038d5d08e646b31bd4a5f5ffc7e19d28e294f3bb3760b' +ALGORITHM=HS256 + +# CORS: Milyen címekről érhető el az API? (Vesszővel elválasztva) +CORS_ORIGINS=https://app.profibot.hu,https://dev.profibot.hu,http://localhost:3000,http://192.168.100.10:3000 + +# Frontend címe a kiküldött linkekhez (Visszaigazolás, jelszó-visszaállítás) +FRONTEND_BASE_URL=http://192.168.100.10:3000 + +# ============================================================================== +# 📧 EMAIL RENDSZER (SMTP / SendGrid) +# ============================================================================== +# EMAIL_PROVIDER lehet: 'smtp' vagy 'sendgrid' vagy 'disabled' +EMAIL_PROVIDER=sendgrid +EMAILS_FROM_EMAIL=info@profibot.hu +EMAILS_FROM_NAME='Profibot Service Finder' + +# SendGrid beállítások +SENDGRID_API_KEY=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 + +# SMTP Fallback (Csak ha az EMAIL_PROVIDER=smtp) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=info@profibot.hu +SMTP_PASSWORD='SAJÁT_APP_PASSWORD' + +# ============================================================================== +# 📦 MINIO (Fájltárolás - NAS-ra kivezetve) +# ============================================================================== +MINIO_ENDPOINT=minio:9000 +MINIO_ROOT_USER=kincses +MINIO_ROOT_PASSWORD='MiskociA74' +MINIO_ACCESS_KEY=kincses +MINIO_SECRET_KEY='MiskociA74' \ No newline at end of file diff --git a/CHANGELOG.md b/archive/old_other/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to archive/old_other/CHANGELOG.md diff --git a/backup_20260128_alap_kesz.sql b/archive/old_other/backup_20260128_alap_kesz.sql similarity index 100% rename from backup_20260128_alap_kesz.sql rename to archive/old_other/backup_20260128_alap_kesz.sql diff --git a/backup_to_nas.sh b/archive/old_other/backup_to_nas.sh similarity index 100% rename from backup_to_nas.sh rename to archive/old_other/backup_to_nas.sh diff --git a/deploy_v16.sh b/archive/old_other/deploy_v16.sh similarity index 100% rename from deploy_v16.sh rename to archive/old_other/deploy_v16.sh diff --git a/docker-compose_2026.02.01.yml b/archive/old_other/docker-compose_2026.02.01.yml similarity index 100% rename from docker-compose_2026.02.01.yml rename to archive/old_other/docker-compose_2026.02.01.yml diff --git a/init_dev.sh b/archive/old_other/init_dev.sh similarity index 100% rename from init_dev.sh rename to archive/old_other/init_dev.sh diff --git a/api_spec.json b/archive/old_specs/api_spec.json similarity index 100% rename from api_spec.json rename to archive/old_specs/api_spec.json diff --git a/api_spec_v2.json b/archive/old_specs/api_spec_v2.json similarity index 100% rename from api_spec_v2.json rename to archive/old_specs/api_spec_v2.json diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc old mode 100755 new mode 100644 index 5b314fe218644900f8dd982ffbaff2d8fb579044..b9bd5132f9e0227e080ac7c3c78d81bf41a21791 GIT binary patch literal 2307 zcmaJ?Uu+ab7@yg{*S+g;++8_n5l$4%3EUMakW>QU+JcRt7)p%ZgUxa?w0qgxJ!iKq zt=OO?N)wW+FQAbG9yQ>buNvD|W74K3y^J&=@kQSbOJe28Z}#qL4eD&>o8Nr%&7be@ z{?OiTAZR-~XRU8kgnki|R@0kc{auXEZA1{kHY$iwvT<1|NLXOmmdi>(39w?TWv!qE zShe+Xs1ORUX1A4%f)QZd4woZ^NPt6jdpTN&Vnijq-NY_pI%hGc|=XyMGg#hBBBZ zOmE657G1}uSNyW+n4^>dTR&rZ{^>Ud0Y>}IUl=}T5n|IxlTqL(LKa5DtXLz=9ruGR zO5XIusz#kr%b{F0E0tnX{oskh!7^BD{U&hVKp33tZ?2b`W(*{or1j(;G>Zv(Uq+S~jgAf(C6nS>F6n&QW z-#0+O!q;-ctn^PUL{NUM+`_jp-fD^1zueGdZR*gp14dKmIJ!9p-Gbf={oo7eFbZx` zz+Qo_$B~LWJP8T(4p<$dnA^GU zq4O8h18)xwUmQ*o(>F71Oc;x+mngWG>+}jO3eRhT8DitkhzKMW3Z`Z1n_?3;ij0~* z9Wia2>%{UZF2sn$go(KVQRjHV@9Ff@SPsG^=w;J#vXvnrp zZCpc7G!)xWy*hWbI&*X8Ua}UASM53bPR~+0ztYvW6763N^)H3`>l)gYsFvr-clz(1 z`~2MM?w;k{JuA_ptD&Pyp`(Aaq1c}4%o78t#zP}k&CX?4jLuc1b6M%Ex5L^-E!4jO zXS5yLs0-PR>P&TJZsut{g4C|39)s6>w5w0SU*P`~hX*R2uN&Ufd@si)l^ZmodmJW&(Nu3(g@ee@b6Gg7e2Bi#@u) zuZ4g=9UrD_!Yb1A8OtG*r6Eh7s)w`P+3smIN8F+}odi2txrxI$w?Z9&*^=iv8I`NP z<=d3&6O?(D>+q0}9vNpg4>u*nBrrFRh^!wue_`Y2>-oJ$ zy1TnMK6sJiS27B32miX0BQCQbHaxz)F$Lcf>iCvvdo2kFj4_vNPRFPTDtOa+^Tnck zR2aQ%UEw&#HCVR&v7q9Zqh1m0z1c+A%|c&438Ya>2qx117Zf5BrIn0Cy(15!ApEr< zuDOm4MT>R7XANN>FtcR&HVKLV!gK`;(ORmSYQR)Nt zE3^&*h$V!W!d%y9Ie-VDdoKYAf)^qx(N#-yK1}VpJ@x6-y^b#0LY?7eYUMj45}dF`ei83;8`Xld8#?fmN%<(w&*gCi3-UL?>*bJ zi(|Ar)kxMTWLj2)#l>Fi|skSTM<@>?J7TjU*1lfa6h5L`#tUL2Lxs%Y;r`^(waI zbG_yGs7c7k->=|N%PHF9gf{;YTm~}*7GdJA#j=6~whEmngSBmXlAQpa_$zon0;$Uw zAxcw)@3Ah1dPW?q(^JUo?0YPYfsi< zFV)&Ro`f}Bx+d3?D4e{pubP?5%nvQS_}YqbV%5km8~N{#tQcpn>5q`SisH*CUQ^<= z_^#TH&RSx}{QiK{!w4syAPr0PIKoOpZoc=PwS?k7q3%am`6TsW>PBMz_4(dwsTI8E Pv7};6JZo=&0(JiamQN+k literal 2237 zcmah~O-vg{6rR~XuMHT3`Dq{}R#TdoNQ+G%B&dxP18JfPQUwl;FKFW#V69!RySwJc zB`6JP8mdTeN?Ivx(_1Q%dv8^^<^rM;)>cSJRi#$y!6}hSIrYu@hqP7IvEIx#Z{NIm z^Zxw2t}cLJeD?Yc?Kc6TpV+1{d}Xkgzz97+1QDzw11B+NnA16fPx2n-b-@smqK5@t zGJHv&hecgZ${;K0ej|_!cs!q8W7H<=JS^)$qdr-W5yiw$`ALohZVPMWYm*@kQJ&P0 zT2e+deXec z{f|@~Y59j#^xwKjD|rp>JW|m{+HRxjTbk#^X#211P!zji)t=9!Qf9`XbB>{A)G2BK z+jmj5oeS?@1{fT?a&>G(BSfdOszre#FxgQ60X3^lSZ2=g&PY`xBvhNayqe9XAY9eK zg2==B_U;1rJ%m{xF$50+c>@BufYW6cRZjq@nB9w(FhL*j2;Ju%@OMNJ-QX6ubR{uf zIpX*$8^W)Z@2{*&kS?AMzN7AR9f)3Yo=nz-Xf8PK}2fs=vmbkya zf+gQm;XZy)@<6zv?{KYnplW;h{TR)oesu2>APRH_=;Uz}N8SyPp!dBu_9GG5c$UNH z8h9L|duQkPYiJgO^czq4SsWEcEtYc-PKjowv<%Hh2CoiZ7$1&|4PF`^xe&R0F*15( zJTm;j<+1Ux2vHq18gPRUT!ma$Y--z@nX#C^T=6;u1+I_I(G*Jxi7{m>mziKmX0f=q zvOyh{DRBcSi>eNtP<7q)5zWq;HjVNYYXT*80;FZehKQp`ByteyhATZXR4t=q=UrLX zCaIlOGgb@8uyghun0`U~H_^{RXQ94%WnpsJ#w~SnQcFdKQ>Hy{JJg7b<+9Yekj+MVl-Pj?lM>4}fh)D{nVxPl zOEUnKv~6ahf-5?jqf^&6LoF6qx2F7kB4_EIpYv_gj*}he?v9`9QQ~LLD)CrM={-Gg zIu?uNo6FyHGo|X&rtNs_aTj-i>N<$DIXhum8f1y>Hk7uI1%zfCP1Ws+1f*)}3juOQ}_3Dj z!b5%xdDS3}x{{gEp$x26kjjhQ#7x;y-j!@eb#k_K1h}rinBOAnB+~|jtY(jGnWkfP z0c2#eTK1B0x25B8|99_h)(mY3Lx0-Lj$1xIjcAsy12z7DVg4BO~Ko2HCp|stHV%7{=d=zYQrLg+h?nKr~8at0NLZVn8YqihuzrWG* zG}+K*#nEpB@CGno6hMePghX8mN}=K@q3Wrj=4qks>7n5nq3M~S&Aq|-0YDJx*m|2 z8_#GE&~}S!&47@Eo1`C+b}SS|q7F;qf#wR7slv3x^@x!s?Q=EpTk$}5r6Ci$ZeY4I zGiHd^a8(1XjiU~2 zC$W4cnJp7!thAS&(uvzC^pwf3z!zwHJ?LVR_4;)F9UP?*@Mp_g*wdFl0&SN1os523OTmABVMwP9_$ zHGVu^`v8@XaQD+c>H278xU$_JzZg>~t>w~k diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index 10d663f..90dbf15 100755 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -1,12 +1,11 @@ from fastapi import APIRouter -from app.api.v1.endpoints import auth, users, vehicles, billing, fleet, expenses, reports +from app.api.v1.endpoints import auth # Fontos a helyes import! api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(users.router, prefix="/users", tags=["users"]) -api_router.include_router(billing.router, prefix="/billing", tags=["billing"]) -api_router.include_router(vehicles.router, prefix="/vehicles", tags=["vehicles"]) -api_router.include_router(fleet.router, prefix="/fleet", tags=["fleet"]) -api_router.include_router(expenses.router, prefix="/expenses", tags=["expenses"]) -api_router.include_router(reports.router, prefix="/reports", tags=["reports"]) \ No newline at end of file +# Minden auth funkciót ide gyűjtünk (Register, Login, Recover) +api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) + +# Itt jönnek majd a további modulok: +# api_router.include_router(users.router, prefix="/users", tags=["Users"]) +# api_router.include_router(fleet.router, prefix="/fleet", tags=["Fleet"]) \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc old mode 100755 new mode 100644 index 07ce30ee47d0e7767e24e76505ba9ab1dd1e96e7..7190b5505361642b2302643e221e3899cf462a61 GIT binary patch literal 1964 zcmZ`(U2IfE6rQ=id-ty`Z7E9)-9o`^4ZDB@h*-=b5I`uktwz^}&35m!y=Cw2GWTvv zYk>+zP4q!yA|z@84=P$nOn4#jg;Enud}#xwoH^&r zIcMg4=lm23`3a1h-<&ZnN`(B12XFYQ#NrA{$XTKjo#u#1Q`8hvf+?m%Q%XsuoRTTZ zgq&ikDb=OLoX^x!noCPLzZpmc%wQ^LhEgGyC+EUuO{#_xfiOB*b(5-PwL|MX=Z|+? zVe(MhNA0I*Q_J3??Ea=*i z^w2Up4Wr>$&F|p*v*WHauldsva>b;}9u@9pi{dpygEwo%ACYBxJpVwUYF zmYuOnR*X8qq&1q)_Atu=1vo1M%ueflG0_RWVlm#$1`G>LYIr(S7%=jV+*>%xP&be) z*@Hcd4;xuFrX_e)7bO+@>1;t~j>@ZE)e_i9g9SnF%ZeCwbzqLqz+Bu9zxNWMkY~I4 zkAw_W|JPH-eOwr)Lx7Zj4=)#I01Iz|hfjpF;ybcTPKe{86|z-NU#GfoN%XW&Q*F6F z4{@F{?C5Wt47n&>#s+dI_~aQ+m!!$+>mJ1+R^yO#Syy!RGogoUCm$S!8CXZ`uxF!< z@HHpoWpVm+&93vLtuvp7Ds5z-)VdZzgT(-?kC5h zY@?Yma?$McWix6{U*OUH!ay#Qv*R&=V*rk#GaEEwB1ezB7I}W7n9k@r2YfiHRmx_8 zC^dn^1;&`or!QoU=_^soILbh&6>Q-rJE{qanE_@jK?Gyro-sfP;CS6M{y-_{<8^mN?&WSfiu%XP~1L4UToiQQ~5QV+t#PTWK8wFaw~> z(KtJ9$1~X0i~`4EcEXU|xZW4HDs7Hq_MBQ|T6iO4#WBkq%bQ~u9ADx@T!l(|t7xPF zeAw(N@Kt!F+e$@5VJ)`W^p;JV(=T@KP4@2HI3<9z?zhK|_{ z9TUwr=&l7HS=DlG_~TOxYCSzd=R*x;5s7=DNLfY#{!?qb?+O=1BD_!*Ty8rNe6G3I lZqSvtw(q(+eC5;v5vX=9QpW9K@IG#MrD;I`ipL?ye*h=Q+>8JK literal 5984 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 diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 565aa85..3677062 100755 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -1,91 +1,34 @@ -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, text -from datetime import datetime, timedelta -import hashlib, secrets - from app.db.session import get_db -from app.models.user import User -from app.core.security import get_password_hash -from app.services.email_manager import email_manager -from app.services.config_service import config +from app.schemas.auth import UserRegister, UserLogin, Token +from app.services.auth_service import AuthService router = APIRouter() -@router.post("/register") +@router.post("/register", status_code=status.HTTP_201_CREATED) async def register( - request: Request, - email: str, - password: str, - first_name: str, - last_name: str, + request: Request, + user_in: UserRegister, db: AsyncSession = Depends(get_db) ): - ip = request.client.host - - # 1. BOT-VÉDELEM - throttle_min = await config.get_setting('registration_throttle_minutes', default=10) - check_throttle = await db.execute(text(""" - SELECT count(*) FROM data.audit_logs - WHERE ip_address = :ip AND action = 'USER_REGISTERED' AND created_at > :t - """), {'ip': ip, 't': datetime.utcnow() - timedelta(minutes=int(throttle_min))}) - - if check_throttle.scalar() > 0: - raise HTTPException(status_code=429, detail="Túl sok próbálkozás. Várj pár percet!") + # 1. Email check + is_available = await AuthService.check_email_availability(db, user_in.email) + if not is_available: + raise HTTPException(status_code=400, detail="Az e-mail cím már foglalt.") - # 2. REGISZTRÁCIÓ - res = await db.execute(select(User).where(User.email == email)) - if res.scalars().first(): - raise HTTPException(status_code=400, detail="Ez az email már foglalt.") + # 2. Process + try: + user = await AuthService.register_new_user( + db=db, + user_in=user_in, + ip_address=request.client.host + ) + return {"status": "success", "message": "Regisztráció sikeres!"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Szerver hiba: {str(e)}") - new_user = User( - email=email, - hashed_password=get_password_hash(password), - first_name=first_name, - last_name=last_name, - is_active=False - ) - db.add(new_user) - await db.flush() - - # 3. TOKEN & LOG - raw_token = secrets.token_urlsafe(48) - token_hash = hashlib.sha256(raw_token.encode()).hexdigest() - await db.execute(text(""" - INSERT INTO data.verification_tokens (user_id, token_hash, token_type, expires_at) - VALUES (:u, :t, 'email_verify', :e) - """), {'u': new_user.id, 't': token_hash, 'e': datetime.utcnow() + timedelta(days=2)}) - - await db.execute(text(""" - INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address) - VALUES (:u, 'USER_REGISTERED', '/register', 'POST', :ip) - """), {'u': new_user.id, 'ip': ip}) - - # 4. EMAIL KÜLDÉS - verify_link = f"http://{request.headers.get('host')}/api/v1/auth/verify?token={raw_token}" - email_body = f"

Szia {first_name}!

Aktiváld a fiókod: {verify_link}

" - - await email_manager.send_email( - recipient=email, - subject="Regisztráció megerősítése", - body=email_body, - email_type="registration", - user_id=new_user.id - ) - - await db.commit() - return {"message": "Sikeres regisztráció! Ellenőrizd az email fiókodat."} - -@router.get("/verify") -async def verify_account(token: str, db: AsyncSession = Depends(get_db)): - token_hash = hashlib.sha256(token.encode()).hexdigest() - query = text("SELECT user_id FROM data.verification_tokens WHERE token_hash = :t AND is_used = False") - res = await db.execute(query, {'t': token_hash}) - row = res.fetchone() - if not row: - raise HTTPException(status_code=400, detail="Érvénytelen aktiváló link") - - await db.execute(text("UPDATE data.users SET is_active = True WHERE id = :id"), {'id': row[0]}) - await db.execute(text("UPDATE data.verification_tokens SET is_used = True WHERE token_hash = :t"), {'t': token_hash}) - await db.commit() - return {"message": "Fiók aktiválva!"} +@router.post("/login") +async def login(user_in: UserLogin, db: AsyncSession = Depends(get_db)): + # ... A korábbi login logika itt maradhat ... + pass \ No newline at end of file diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc old mode 100755 new mode 100644 index 265415ea3e251ed68c5e87cf316265c5da5c4709..48a77db0aee79d8ac6ade072493153472ba437e6 GIT binary patch literal 3410 zcmaJDOKcm*b(XvQiu%#|QPhW)9LY>=O7bVMRm%!3t!2d$sd8yMB2sKu+@ZD7l1uL{ zWs{KsrFIJSbC50-b&_FYQgPeQ1jwIRaRTsHLexHM6?v;;WDAHuq+}4Uj8vS$v_-b0=s2BX z;!MgKw^C${mTf6}+@5m89VX4l&Xg583ry#>Jf zr&Xizh8z`e{GCwY0FCHSI)@yQj4ml@K}O7sr0D?*a$gZN$U$9~(zi9_sbLdJIw{?r zloEP?LW~aYk!2H3vmdF^CDzq6 z#_LTFT&6$nc!NpRn^Z%*5jJ66jYsNgdP-INh&P+EO-FJ!i=G3^9+SWMNd9AD%K`r} zlmFO}{HptM_JY=u?NSU4k8Q&q9o+D*?MSsn|c$c|%y@X#pCHBE?u^%$x z3D_gP2YZqZap0jf-isski-Y1xF(AJG`yV}Js#$S7cRX+!IcoPR-x*VdWKv4_*@U9y zG##e=Ts8yMP$q-ibJ57h>_m(Yhi2KlbtE_(97gt=Y?Pahg!A?xAtMc~j38HNZkoS2 z!gH}GvQDyBZcHKTRYBGuzNl&zA{WO_MA;aBot;Mw;Yf_vMc;bK*EE)tDN*Kz5n(leunveOK;J?uv)==>#B9B)?E$> z){5bM+p0}4jyP= z^LZ%yv67|Yi)BBQPx%*>+c~Uax%j2%FaBXk6^hT);+`h>)5Rz9a`6Rb5yX^~CVKob zT*U1Mru_#Bi+i$Me5MO&;?+`C7i1;nPYW4kS&R8L@5^*sUxh>-P87OFXtOd3flT2=09cDzvC_kXwJ%bzj- zgrdTsgxQ{hnH*}k4Ru~zsO@^by;f<0|GV%$R;DQ!{0;nb4eYxePT!wfI!fM8jh3IT zvt?iR&gk~&E_@81e^NSq+3=0OZXI90TK05o4Q&n?p8oZTuNyrZT47{!rPLlMw|5e3 zqrQBsb3MH81Z^Kt|9a;FP2PV1%IU+izY6cu82gVliEaLz3UyHr`$Bc#cT~v9JnF^R zOBOfEUkt8VN)qBUJbjXW=;(~2&T2b6}F>hY4=K-MjahHQAdv3X>bmAg$^1^7$HxwV;%euFuO4{B{{oimuua$E1( z#~ZZ4^p-uojlQkGW}q;&d&}^ge(gE?%5&E6od2rrm1lg7T|Z|q7{0|D1`%mqjd315_;$zS!*Z& literal 1756 zcmYjR&2Jh<6yJpb76*S|jIqIvadG0(Du5H$P24mpfsIYVmv+}_yO$|phh!z#rMrt2 zqedcXE4k{$In~EptK>iF#S)j*NKsSulv}FgRH;&D28cY;zJ0&nd-G=Ao0<2=^z;-* z#!LJ}zvAb(zbTju-&lE9C(3V}#%X+;>+mYiQ{B_{biAs!1607Ux9#irRX@*pxIIn- zKXIB5d0%)(G1P#ELxC6LfU0ohPY2#ml;*Ke>pHq=G}>to4&83pNJfsM8xL(fKhX-N z(b69l^`=AoXPRcG+jEcxTRLiM&2donBPp=)uFBIbBTnTtPW2p+t*c%HkWcg6Bo3Na z^)mz*@~Hua{0s$D0R@StKoR4EjGt1c7`nhvNWFkU#A}+N8FiYW*$;E(7#~(=*qnKW zBI+zd3k*foIffP)im73SmKa)A=TQWKWx4~O79Vp9=ePvpR@5k4cXjNsc+TVEn2Vj` z))<#mmsp1D41J_7vz1d6kzo;sa<}#`2rA%3^j4IocCL3r zH0>kXL7k-B>mn=P?cz{fs@)X}3asWUV&8W)lgs3A;JzryrCPNg$Tqrq_HYYNjmZ6hv8uO6{Il zh2l5$k|e@Psd}J@@*BEc9I5AJ`Rkfgq%*RW@IWf}Qx?*6dpDc4 zk#(py5xGf*hAcRucdmVYBb&oPNi3FR>W@LA=a_g#tmI2&NYcAsDvDBnPB&WSS6$0& z=?_dN^Qecz%tMBEq*?_s9Silb9nYON(%Mwwdau`NE2If z{S3rT2}lwUSsa2W`7eGk^4)zaaEbLn^`t=TuUE5YAbLVVWZjj^XJFx^IRux9_vWV* zJUX?Xb%x**8gqk8e|+lw<;f7F-00FM*pj=JI?tyz2bGiSr-f%<4#B3oyh^g!7=l$Y z|Kjd=p&NfiF1g{D8((*033q1kebBeye_FP3lX_IsLF{*qy5v%@(&KRVNNX65-XynU z)W$9PDn~^ZCPs@kYF1DzeDKRsjz!)g8WwJj?$0f2hd|QLIeUYMw_cv-UvY^)xm&Ne b)O(QO{l`DN=g9A$?|3e>@ayhVfkpWr;U?JR diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc old mode 100755 new mode 100644 index a8012462b7de21a213336a94c3da5ffce97630f1..5854f3671071f7d6320e3f754df100b80b2337f3 GIT binary patch delta 545 zcmZ1`v{r=gG%qg~0}wRtYR){)x{;5G$*a^A$mdc3g2U5_i>p#I5{vWmat^O8$x2j6 zRLHALEh<*XO3f**I=s0AC;*hs&`r%LQ7AjSFsCHHDzT&_F-H%qf3hG`5id8;v|>LX z(ZKM5WAg+iDMqVOQ|0|SU& z!TcFae-z?m3}Kv+vcPx+^8$;Rc^?=+{2dVoGB4T&KsXm-;y;5$KZvm~1~bk8ngF5~ Vuq+5(p}ZhsA>S7;zeo<~5&)ajrEUNK delta 299 zcmZ20vP_8YG%qg~0}x!gRh!w#vXPI8iP3Lz6jKpcwAc?Q)4=e7Y4c4cDMp}#E=x3@ zC{V1}7%XvzTX6FXmOhY7HQNO{DUeJuL`FblLdayU2G2XZqWv+QF>^HL>&(=-%&We_ z_L6q-9?vrw*Zqnv`W0QVDZa>C(%^Q3g?I7<_Cr8}Dmnc46hH>4Kn&vMn|z()Mjaa` zqv!(X4-7zRhccKtpz;~S`yj@|=*hT%`2z!pUcvksOn>BJVGLoMk+Q&e1@i)nnRy== yK>QsM2Qn|(20%C$V&Xr8ML+N`G6plw0Ga@z7qBb{UZK1oVj laza, PROD -> szigorú) --- - PASSWORD_MIN_LENGTH: int = 4 # TESZT: 4, ÉLES: 10-12 + # --- Database & Cache --- + DATABASE_URL: str = os.getenv("DATABASE_URL") + REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0") - # --- Database --- - DATABASE_URL: str # már nálad compose-ban meg van adva - - # --- Redis --- - REDIS_URL: str = "redis://service_finder_redis:6379/0" - - # --- Email sending --- - # auto = ha van SENDGRID_API_KEY -> sendgrid api, különben smtp - EMAIL_PROVIDER: str = "auto" # auto | sendgrid | smtp | disabled - - EMAILS_FROM_EMAIL: str = "info@profibot.hu" + # --- Email (Auto Provider) --- + EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto") + EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "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 - SENDGRID_API_KEY: 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") - # SMTP fallback (pl. Gmail App Password vagy más szolgáltató) - SMTP_HOST: Optional[str] = None - SMTP_PORT: int = 587 - SMTP_USER: Optional[str] = None - SMTP_PASSWORD: Optional[str] = None - SMTP_USE_TLS: bool = True - - # Frontend base URL a linkekhez (később NPM/domain) - FRONTEND_BASE_URL: str = "http://192.168.100.43: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}) + row = result.fetchone() + if row and row[0] is not None: + return row[0] + return default + except Exception: + return default + # .env fájl konfigurációja model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", @@ -50,4 +63,4 @@ class Settings(BaseSettings): extra="ignore" ) -settings = Settings() +settings = Settings() \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 84e8621..fc8cd03 100755 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -6,28 +6,44 @@ from jose import jwt, JWTError from app.core.config import settings -# --- JELSZÓ --- +# --- JELSZÓ KEZELÉS --- + def verify_password(plain_password: str, hashed_password: str) -> bool: + """ + Összehasonlítja a nyers jelszót a hash-elt változattal. + """ try: if not hashed_password: return False return bcrypt.checkpw( plain_password.encode("utf-8"), - hashed_password.encode("utf-8"), + hashed_password.encode("utf-8") ) except Exception: return False def get_password_hash(password: str) -> str: + """ + Biztonságos hash-t generál a jelszóból. + """ salt = bcrypt.gensalt() return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8") -# --- JWT --- +# --- JWT TOKEN KEZELÉS --- + def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """ + JWT Access tokent generál a megadott adatokkal és lejárati idővel. + """ to_encode = dict(data) - expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)) + expire = datetime.now(timezone.utc) + ( + expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + ) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def decode_token(token: str) -> Dict[str, Any]: - return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + """ + Dekódolja a JWT tokent. + """ + return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index a98d4fd..f14c089 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,45 +1,50 @@ +import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text -import os from app.api.v1.api import api_router -from app.api.v2.auth import router as auth_v2_router -from app.models import Base +from app.db.base import Base +from app.db.session import engine @asynccontextmanager async def lifespan(app: FastAPI): - from app.db.session import engine + # Séma és alap táblák ellenőrzése indításkor async with engine.begin() as conn: await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data")) + # Base.metadata.create_all helyett javasolt az Alembic, + # de fejlesztési fázisban a run_sync biztonságos await conn.run_sync(Base.metadata.create_all) yield await engine.dispose() app = FastAPI( - title="Traffic Ecosystem SuperApp 2.0", - version="2.0.0", - openapi_url="/api/v2/openapi.json", + title="Service Finder API", + version="1.0.0", + docs_url="/docs", + openapi_url="/api/v1/openapi.json", lifespan=lifespan ) +# BIZTONSÁG: CORS beállítások .env-ből +# Ha nincs megadva, csak a localhost-ot engedi +origins = os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",") + app.add_middleware( CORSMiddleware, - allow_origins=[ - "http://192.168.100.43:3000", # A szerver címe a böngészőben - "http://localhost:3000", # Helyi teszteléshez - ], + allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) - -# ÚTVONALAK INTEGRÁCIÓJA +# ÚTVONALAK KONSZOLIDÁCIÓJA (V2 törölve, minden a V1 alatt) app.include_router(api_router, prefix="/api/v1") -app.include_router(auth_v2_router, prefix="/api/v2/auth") @app.get("/", tags=["health"]) async def root(): - return {"status": "online", "version": "2.0.0", "docs": "/docs"} - \ No newline at end of file + return { + "status": "online", + "version": "1.0.0", + "environment": os.getenv("ENV", "production") + } \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index cfd7330..3bffbb8 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,6 +1,7 @@ from app.db.base import Base -from .user import User, UserRole +from .identity import User, Person, Wallet, UserRole # ÚJ központ from .company import Company, CompanyMember, VehicleAssignment +from .organization import Organization, OrgType from .vehicle import ( Vehicle, VehicleOwnership, @@ -13,12 +14,12 @@ from .vehicle import ( VehicleVariant ) -# Alias a kompatibilitás kedvéért +# Aliasok a kompatibilitás kedvéért UserVehicle = Vehicle __all__ = [ - "Base", "User", "UserRole", "Vehicle", "VehicleOwnership", "VehicleBrand", - "EngineSpec", "ServiceProvider", "ServiceRecord", "Company", + "Base", "User", "Person", "Wallet", "UserRole", "Vehicle", "VehicleOwnership", + "VehicleBrand", "EngineSpec", "ServiceProvider", "ServiceRecord", "Company", "CompanyMember", "VehicleAssignment", "UserVehicle", "VehicleCategory", - "VehicleModel", "VehicleVariant" + "VehicleModel", "VehicleVariant", "Organization", "OrgType" ] \ 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 old mode 100755 new mode 100644 index 4e78382c4b628f8c88d2c198e610b5ac5c0574df..46a370855b5ad702a1b8f14f39736f1adc4bdb48 GIT binary patch delta 496 zcmZXQyGjE=6ozLudrPvL`zBGuC-WMde-x^grPRrIASR2uN-0jL@z@}k1dCu#H?+;`I7xSUYMfS(X)-ty7hxw7af!+v zp-gZHuEIfmRi^#Hpb@kiZ5401U)o_HdZTbO{^z7dq}A!;!0(NJ8y++1^nc|3_K?=) zSBebTW(he$o=_kZ34bzHrY&u6Yg2XT%Z5#bj|AuZjjhdD^OZH{to6cLbG9?R*Vgr# G>VEZrrZdTafTv%)B>(nbdy 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 literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f38794999b6109f332da42520f1e59e4a19b6b7 GIT binary patch literal 1653 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 UhRz3Q{}bT^$L)KBXvq}*54hXdQvd(} literal 0 HcmV?d00001 diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..99cec23 --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,81 @@ +from datetime import datetime, timezone +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, and_, text + +from app.models.identity import User, Person, Wallet +from app.models.organization import Organization, OrgType +from app.schemas.auth import UserRegister +from app.core.security import get_password_hash +from app.services.email_manager import email_manager + +class AuthService: + @staticmethod + async def register_new_user(db: AsyncSession, user_in: UserRegister, ip_address: str): + """ + Master Book v1.0 szerinti atomikus regisztrációs folyamat. + """ + # Az AsyncSession.begin() biztosítja az ATOMICitást + async with db.begin_nested(): # beágyazott tranzakció a biztonságért + # 1. Person létrehozása (Identity Level) + new_person = Person( + first_name=user_in.first_name, + last_name=user_in.last_name + ) + db.add(new_person) + await db.flush() # ID generáláshoz + + # 2. User létrehozása (Technical Access) + new_user = User( + email=user_in.email, + hashed_password=get_password_hash(user_in.password), + person_id=new_person.id, + is_active=True + ) + db.add(new_user) + await db.flush() + + # 3. Economy: Wallet inicializálás (0 Coin, 0 XP) + new_wallet = Wallet( + user_id=new_user.id, + coin_balance=0.00, + xp_balance=0 + ) + db.add(new_wallet) + + # 4. Fleet: Automatikus Privát Flotta létrehozása + new_org = Organization( + name=f"{user_in.last_name} {user_in.first_name} saját flottája", + org_type=OrgType.INDIVIDUAL, + owner_id=new_user.id + ) + db.add(new_org) + + # 5. Audit Log (SQLAlchemy Core hívással a sebességért) + audit_stmt = text(""" + INSERT INTO data.audit_logs (user_id, action, endpoint, method, ip_address, created_at) + VALUES (:uid, 'USER_REGISTERED', '/api/v1/auth/register', 'POST', :ip, :now) + """) + await db.execute(audit_stmt, { + "uid": new_user.id, + "ip": ip_address, + "now": datetime.now(timezone.utc) + }) + + # 6. Üdvözlő email (Subject paraméter nélkül - Spec v1.1) + try: + await email_manager.send_email( + recipient=user_in.email, + template_key="registration", + variables={"first_name": user_in.first_name}, + user_id=new_user.id + ) + except Exception: + pass # Email hiba ne állítsa meg a tranzakciót + + return new_user + + @staticmethod + async def check_email_availability(db: AsyncSession, email: str) -> bool: + query = select(User).where(and_(User.email == email, User.is_deleted == False)) + result = await db.execute(query) + return result.scalar_one_or_none() is None \ No newline at end of file diff --git a/docs/1_PROJECT_BRAIN_FLEET.md b/docs/Old_versions/1_PROJECT_BRAIN_FLEET.md similarity index 100% rename from docs/1_PROJECT_BRAIN_FLEET.md rename to docs/Old_versions/1_PROJECT_BRAIN_FLEET.md diff --git a/docs/2_MODULE_STATUS_FLEET.md b/docs/Old_versions/2_MODULE_STATUS_FLEET.md similarity index 100% rename from docs/2_MODULE_STATUS_FLEET.md rename to docs/Old_versions/2_MODULE_STATUS_FLEET.md diff --git a/docs/3_IMPLEMENTED_FEATURES.md b/docs/Old_versions/3_IMPLEMENTED_FEATURES.md similarity index 100% rename from docs/3_IMPLEMENTED_FEATURES.md rename to docs/Old_versions/3_IMPLEMENTED_FEATURES.md diff --git a/docs/4_BACKLOG_FLEET.md b/docs/Old_versions/4_BACKLOG_FLEET.md similarity index 100% rename from docs/4_BACKLOG_FLEET.md rename to docs/Old_versions/4_BACKLOG_FLEET.md diff --git a/docs/5_TECH_DEBT_FLEET.md b/docs/Old_versions/5_TECH_DEBT_FLEET.md similarity index 100% rename from docs/5_TECH_DEBT_FLEET.md rename to docs/Old_versions/5_TECH_DEBT_FLEET.md diff --git a/docs/6_ROADMAP_FLEET.md b/docs/Old_versions/6_ROADMAP_FLEET.md similarity index 100% rename from docs/6_ROADMAP_FLEET.md rename to docs/Old_versions/6_ROADMAP_FLEET.md diff --git a/docs/AI üzemeltetése.md b/docs/Old_versions/AI üzemeltetése.md similarity index 100% rename from docs/AI üzemeltetése.md rename to docs/Old_versions/AI üzemeltetése.md diff --git a/docs/DB_STATE_FLEET_2026-01-28.md b/docs/Old_versions/DB_STATE_FLEET_2026-01-28.md similarity index 100% rename from docs/DB_STATE_FLEET_2026-01-28.md rename to docs/Old_versions/DB_STATE_FLEET_2026-01-28.md diff --git a/docs/Naplócsomag b/docs/Old_versions/Naplócsomag similarity index 100% rename from docs/Naplócsomag rename to docs/Old_versions/Naplócsomag diff --git a/docs/Projekt értékelés.md b/docs/Old_versions/Projekt értékelés.md similarity index 100% rename from docs/Projekt értékelés.md rename to docs/Old_versions/Projekt értékelés.md diff --git a/Promptok gemekhez.txt b/docs/Old_versions/Promptok gemekhez.txt similarity index 100% rename from Promptok gemekhez.txt rename to docs/Old_versions/Promptok gemekhez.txt diff --git a/Service_finder Rendszerspecifikáció es feljesztes.txt b/docs/Old_versions/Service_finder Rendszerspecifikáció es feljesztes.txt similarity index 100% rename from Service_finder Rendszerspecifikáció es feljesztes.txt rename to docs/Old_versions/Service_finder Rendszerspecifikáció es feljesztes.txt diff --git a/_Adatbázis_állalot_napló.txt b/docs/Old_versions/_Adatbázis_állalot_napló.txt similarity index 100% rename from _Adatbázis_állalot_napló.txt rename to docs/Old_versions/_Adatbázis_állalot_napló.txt diff --git a/_Horgony_megjegyzések.txt b/docs/Old_versions/_Horgony_megjegyzések.txt similarity index 100% rename from _Horgony_megjegyzések.txt rename to docs/Old_versions/_Horgony_megjegyzések.txt diff --git a/_Projekt Állapot jelentés.txt b/docs/Old_versions/_Projekt Állapot jelentés.txt similarity index 100% rename from _Projekt Állapot jelentés.txt rename to docs/Old_versions/_Projekt Állapot jelentés.txt diff --git a/_valtozok_konyve.txt b/docs/Old_versions/_valtozok_konyve.txt similarity index 100% rename from _valtozok_konyve.txt rename to docs/Old_versions/_valtozok_konyve.txt diff --git a/lista.txt b/docs/Old_versions/lista.txt similarity index 100% rename from lista.txt rename to docs/Old_versions/lista.txt diff --git a/mappak.txt b/docs/Old_versions/mappak.txt similarity index 100% rename from mappak.txt rename to docs/Old_versions/mappak.txt diff --git a/projekt_terkep.txt b/docs/Old_versions/projekt_terkep.txt similarity index 100% rename from projekt_terkep.txt rename to docs/Old_versions/projekt_terkep.txt diff --git a/docs/teljes_log b/docs/Old_versions/teljes_log similarity index 100% rename from docs/teljes_log rename to docs/Old_versions/teljes_log diff --git a/docs/V01_chatgpt/00_README.md b/docs/V01_chatgpt/00_README.md new file mode 100644 index 0000000..a1a365a --- /dev/null +++ b/docs/V01_chatgpt/00_README.md @@ -0,0 +1,15 @@ +# Master Grand Book v1.0 – Service Finder / Traffic Ecosystem SuperApp + +Ez a dokumentáció a projekt **kanonikus tudásbázisa**. + +Két párhuzamos könyvtár létezik: +- V01_chatgpt – technikai, mérnöki, architekturális megközelítés +- V01_gemini – alternatív gondolkodás, validáció, kiegészítő perspektíva + +Cél: +- Tudás megőrzése +- Döntések visszakövethetősége +- Fejlesztési minőség mérése (kód + beállítás + hibajavítás hatékonyság) +- Új projektek benchmark alapja + +Ez a v1.0 verzió a **baseline állapot** dokumentálása. diff --git a/docs/V01_chatgpt/01_Project_Overview.md b/docs/V01_chatgpt/01_Project_Overview.md new file mode 100644 index 0000000..bb01b21 --- /dev/null +++ b/docs/V01_chatgpt/01_Project_Overview.md @@ -0,0 +1,24 @@ +# Projekt áttekintés + +Projekt neve: Traffic Ecosystem SuperApp 2.0 (Service Finder) + +Cél: +Egy moduláris platform létrehozása, amely: +- kezeli a járművek életciklusát, +- nyilvántartja a költségeket, eseményeket, szervizeket, +- összeköti a felhasználókat valós szolgáltatókkal, +- automatizált adatgyűjtést végez (discovery botok), +- skálázható SaaS modellben működik. + +Fő modulok: +- Auth / User / Organization +- Fleet & Vehicle Lifecycle +- Service Provider Marketplace +- Billing / Credits / Subscription +- Gamification & Social +- Discovery Bots (adatgyűjtés) +- Dokumentumfeldolgozás (OCR pipeline – tervezett) + +Non-goals (v1.0): +- Teljes üzleti automatizmus +- Külső fizetési gateway éles integráció diff --git a/docs/V01_chatgpt/02_Architecture_System_Context.md b/docs/V01_chatgpt/02_Architecture_System_Context.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/V01_chatgpt/03_Dev_Environment_Runbook.md b/docs/V01_chatgpt/03_Dev_Environment_Runbook.md new file mode 100644 index 0000000..f1cf55b --- /dev/null +++ b/docs/V01_chatgpt/03_Dev_Environment_Runbook.md @@ -0,0 +1,19 @@ +# Fejlesztői környezet + +Indítás: +- docker compose up -d + +Alapszolgáltatások: +- API: :8000 +- Frontend: :3001 +- MinIO: :9000 / :9001 +- Redis: belső háló + +Tipikus ellenőrzések: +- API online: GET / +- OpenAPI: /api/v2/openapi.json +- Frontend betölt + +Ismert jellegzetességek: +- v1 és v2 API párhuzamosan él +- .env alapú konfiguráció diff --git a/docs/V01_chatgpt/06_Database_Guide.md b/docs/V01_chatgpt/06_Database_Guide.md new file mode 100644 index 0000000..c6dc3b0 --- /dev/null +++ b/docs/V01_chatgpt/06_Database_Guide.md @@ -0,0 +1,26 @@ +# Adatbázis – Baseline állapot + +DB: PostgreSQL (shared-postgres) + +Séma: data + +Táblák száma: ~55 + +Kulcs entitások: +- users +- persons +- companies +- vehicles, vehicle_models, vehicle_variants +- service_providers, service_specialties +- fuel_stations +- credit_logs, vouchers, subscriptions +- audit_logs + +Migráció: +- Alembic +- Head rev: 5aed26900f0b +- Persons + owner_person_id implementálva + +Seed: +- fuel_stations ~7300 +- service_providers ~7200 diff --git a/docs/V01_chatgpt/07_API_Guide.md b/docs/V01_chatgpt/07_API_Guide.md new file mode 100644 index 0000000..f6e759f --- /dev/null +++ b/docs/V01_chatgpt/07_API_Guide.md @@ -0,0 +1,13 @@ +# API – Áttekintés + +Verziók: +- v1: üzleti modulok (fleet, billing, reports) +- v2: auth és új generációs endpointok + +Elvek: +- JWT alapú auth +- Verziózott API +- OpenAPI dokumentált + +Megjegyzés: +A v1 → v2 egységesítés külön roadmap tétel. diff --git a/docs/V01_chatgpt/13_Roadmap_Tech_Debt.md b/docs/V01_chatgpt/13_Roadmap_Tech_Debt.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/V01_chatgpt/14_Anchor_Log_Timeline.md b/docs/V01_chatgpt/14_Anchor_Log_Timeline.md new file mode 100644 index 0000000..0ddf062 --- /dev/null +++ b/docs/V01_chatgpt/14_Anchor_Log_Timeline.md @@ -0,0 +1,10 @@ +# Anchor Log – döntési napló + +Ez a fejezet rögzíti: +- fontos architekturális döntéseket, +- API-szerződés változásokat, +- adatmodell átalakításokat, +- stratégiai irányváltásokat. + +Cél: +- később visszakövethető legyen, miért úgy épült a rendszer, ahogy. diff --git a/docs/V01_chatgpt/15_Changelog.md b/docs/V01_chatgpt/15_Changelog.md new file mode 100644 index 0000000..8e7f607 --- /dev/null +++ b/docs/V01_chatgpt/15_Changelog.md @@ -0,0 +1,6 @@ +# Changelog + +v1.0 – Baseline +- Mester dokumentum struktúra létrehozva +- DB baseline rögzítve +- API verziózás dokumentálva diff --git a/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md new file mode 100644 index 0000000..1e9be66 --- /dev/null +++ b/docs/V01_gemini/05_AUTH_AND_IDENTITY_SPEC.md @@ -0,0 +1,40 @@ +# 🔐 AUTHENTICATION & IDENTITY SPECIFICATION (v1.0) + +## I. AZONOSÍTÁSI STRATÉGIA +A rendszer szétválasztja a **technikai hozzáférést** (User) és a **valós identitást** (Person). + +### 1. Identitás szintek +- **User (Login):** Email + Jelszó. Csak a belépéshez és a munkamenethez kell. +- **Person (Identity):** Vezetéknév, Keresztnév, Anyja neve, Születési adatok, Okmányok. +- **Azonosító:** Minden Person kap egy globális egyedi azonosítót (UUID). + +### 2. Soft Delete & Re-regisztráció +- **Nincs fizikai törlés:** A felhasználó csak egy `is_hidden` vagy `deleted_at` flag-et kap. +- **Ismételt regisztráció:** Ha az email/név/okmány alapján a rendszer felismeri a visszatérőt: + - Új technikai User fiók jön létre. + - Ez az új fiók a korábbi Person ID-hoz kapcsolódik. + - **Adat-izoláció:** A felhasználó csak az új regisztráció dátuma utáni eseményeket látja. A régi adatok a háttérben maradnak (statisztika, sofőr elemzés), de számára rejtettek. + +## II. BŐVÍTETT ADATTÁR (KYC & SAFETY) +A `persons` tábla az alábbi adatcsoportokat tartalmazza (Progresszív feltöltéssel): +- **Alapadatok:** `last_name`, `first_name`, `birth_place`, `birth_date`, `mothers_name`. +- **Hivatalos okmányok:** Személyi ig. szám, Jogosítvány (szám + kategóriák + érvényesség), Lakcímkártya, TAJ, Adóazonosító. +- **Vészhelyzeti adatok (Safety):** Vércsoport, Allergia, Értesítendő személy (ICE) neve és telefonszáma. +- **Jutalom:** A teljes körű adategyeztetésért 2 hét PRÉMIUM tagság jár. + +## III. JUTALÉK ÉS GAZDASÁG +### 1. Piramis rendszer (3 szint) +Meghívó lánc alapján számolt jóváírás: +- **1. szint (Közvetlen):** 10% +- **2. szint:** 5% +- **3. szint:** 2% +*A százalékok a befizetés pillanatában érvényes admin beállítások alapján rögzülnek a tranzakcióban (Snapshot).* + +### 2. Wallets +Minden regisztrációnál létrejön: +- **Coin Wallet:** Belső fizetőeszköz (Kredit). +- **XP Ledger:** Tapasztalati pontok (Verseny és rangsor). + +## IV. MODERÁCIÓ ÉS VALIDÁLÁS +- **Validált vélemény:** Csak igazolt ott-tartózkodás (GPS) vagy számlafotó után adható. +- **Fellebbezés:** A szerviz kérheti a vélemény felülvizsgálatát, amit a Moderátorok/Validátorok bírálnak el. \ No newline at end of file diff --git a/docs/V01_gemini/05_Security_Model.md b/docs/V01_gemini/05_Security_Model.md deleted file mode 100644 index 41cc9cc..0000000 --- a/docs/V01_gemini/05_Security_Model.md +++ /dev/null @@ -1,17 +0,0 @@ -(Biztonság és Identitás.) -# 🔐 SECURITY & IDENTITY MODEL - -## 1. Identitás Kezelés (Person vs User) -- **Person:** Természetes személy (GDPR alany). `deleted_at` esetén nem töröljük, csak minden személyes adatmezőt (név, email, tel) hashelünk/anonimizálunk, de a statisztikai ID megmarad. -- **User:** Belépési fiók. Egy Person-höz több User tartozhat. -- **Company:** Céges entitás. Tulajdonosa egy Person. - -## 2. Authentication -- **Token:** JWT (JSON Web Token) HS256. -- **Password:** Argon2 hash. -- **Anti-Enumeration:** "Ha létezik ilyen email cím, küldtünk egy levelet" (nem áruljuk el, hogy regisztrált-e). - -## 3. Soft Delete Logika -Minden táblában (`users`, `vehicles`, `events`) kötelező a `deleted_at`. -- **API szinten:** Minden lekérdezéshez automatikusan hozzáadódik a `WHERE deleted_at IS NULL`. -- **Admin szinten:** Láthatóak a törölt elemek is (Audit célból). \ No newline at end of file diff --git a/docs/V01_gemini/06_Database_Guide.md b/docs/V01_gemini/06_Database_Guide.md index 37fae90..0ecfcaf 100644 --- a/docs/V01_gemini/06_Database_Guide.md +++ b/docs/V01_gemini/06_Database_Guide.md @@ -1,6 +1,26 @@ (Az Adatbázis Bibliája.) # 🗄️ DATABASE GUIDE +# 🗄️ DATABASE GUIDE & DATA INTEGRITY (v1.0) + +## 1. Soft Delete & Újraregisztráció Logika +A rendszerben nincs fizikai törlés. A `data.users` tábla az alábbi módon kezeli a visszatérő felhasználókat: + +- **Indexelés:** Az `email` mezőn egy *Partial Unique Index* (`idx_user_email_active_only`) található. +- **Működés:** - Ha `is_deleted = FALSE`, az email nem használható újra. + - Ha a felhasználó törli magát (`is_deleted = TRUE`), az email felszabadul. + - Új regisztrációkor a rendszer új `user_id`-t generál, de ha a KYC adatok egyeznek, ugyanahhoz a `person_id`-hoz kapcsolja az új fiókot. + +## 2. Person (Személyazonosság) - KYC & Safety +A `data.persons` tábla tárolja a banki szintű azonosításhoz szükséges adatokat: +- **Szétválasztott nevek:** `last_name` és `first_name` a pontos azonosításhoz. +- **JSONB mezők:** Rugalmas adatszerkezet az okmányokhoz (`identity_docs`) és vészhelyzeti adatokhoz (`medical_emergency`). +- **Jutalom Trigger:** A profil 100%-os kitöltése (név, szül. adatok, okmányok) automatikusan aktiválja a 14 napos PRÉMIUM csomagot. + +## 3. Economy (Pénztárca & Referral) +- **Wallet:** Minden regisztrációkor létrejön egy rekord a `data.wallets` táblában (0 Coin, 0 XP). +- **Referral Snapshot:** A jutalékok kifizetésekor a rendszer rögzíti a tranzakció pillanatában érvényes százalékot (`commission_percentage`), így a későbbi admin módosítások nem érintik a múltbeli elszámolásokat. + ## Sémák - `public`: Csak technikai táblák (pl. Alembic version). - `data`: Az üzleti logika 55 táblája. @@ -14,4 +34,16 @@ ## Migrációs Állapot - **Eszköz:** Alembic. - **Current Head:** `10b73fee8967`. -- **Hiányzó láncszem:** A `persons` tábla létrehozása és a meglévő `users` tábla migrációja (Ba \ No newline at end of file +- **Hiányzó láncszem:** A `persons` tábla létrehozása és a meglévő `users` tábla migrációja (Ba + +## 4. Regionalizáció és Multi-Currency (EU Scope) +A rendszer fel van készítve az EU-s piacra: +- **`data.regional_settings`**: Tárolja az országkódokat (ISO 3166-1), az alapértelmezett nyelvet és a helyi pénznemet. +- **`data.exchange_rates`**: Napi frissítésű váltószámok (Base: EUR). +- **Valuta Logika:** - Minden költséget a rögzítéskori **helyi pénznemben** (`currency_code`) és az akkori váltószámmal átszámított **EUR-ban** is elmentünk. + - Képlet: $$Cost_{EUR} = Cost_{Local} \cdot ExchangeRate$$ + - Ez biztosítja, hogy a nemzetközi flották egységes kimutatást kapjanak. + +## 5. Dinamikus Paraméterezés (System Settings) +- **`auth.reward_days`**: Adminból állítható egész szám (alapértelmezett: 14). +- **`auth.reward_tier`**: Melyik csomagot kapja (alapértelmezett: PREMIUM). \ No newline at end of file diff --git a/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md new file mode 100644 index 0000000..45c8dab --- /dev/null +++ b/docs/V01_gemini/07_REGISTRATION_INVITATION_AND_API.md @@ -0,0 +1,52 @@ +# 🏁 REGISZTRÁCIÓS ÉS AUTH PROTOKOLL (v1.1) + +## 1. Hibakezelési Jegyzet (TypeError fix) +A rendszer korábbi verzióiban az `EmailManager` hívása paraméter-eltérést okozott. +- **Megoldás:** A `send_email` hívásakor tilos a `subject` paraméter átadása, mivel azt a szerviz a `template_key` alapján generálja a belső szótárából. + +## 2. Adatbázis Integritás +Az `Organization` tábla bővült az `owner_id` mezővel, amely a magánszemély (Individual) flottájának tulajdonosát jelöli. +- Minden regisztrációkor létrejön egy automatikus flotta. +- A flotta típusa: `OrgType.INDIVIDUAL`. + +## 3. Dinamikus Paraméterek +A regisztrációt követő jutalmak (pl. 14 napos prémium) a `data.system_settings` táblából kerülnek kiolvasásra. +Keresett kulcs: `auth.reward_days`. + +# 🏁 REGISZTRÁCIÓ, MEGHÍVÓK ÉS API PROTOKOLL (v1.0) + +## 1. Regisztrációs Flow (Atomcsapás-biztos tranzakció) +Minden új regisztráció egyetlen adatbázis-tranzakcióban (`Atomic`) hajtja végre az alábbiakat: +1. **User & Person létrehozása:** Alapidentitás rögzítése. +2. **Wallet inicializálás:** 0 Coin és 0 XP egyenleggel. +3. **Privát Flotta (Private Org):** Létrejön a felhasználó saját cége, ahol ő a tulajdonos. +4. **Meghívó feldolgozása:** - Ha `Personal Invite`: Bekötés a 10-5-2% jutalék láncba. + - Ha `Company Invite`: Másodlagos kapcsolat létrehozása a meghívó céghez (Role: Driver/Admin). + +## 2. Meghívó Küldés Logikája (Invitation Engine) +- **Generálás:** Admin vagy jogosult User generál egy egyedi `invite_token`-t. +- **Típusok:** + - `REG_ONLY`: Csak a rendszerbe hív. + - `COMPANY_JOIN`: Meghatározott cégbe és pozícióba hív. +- **Jutalék számítás:** + A jóváírandó kredit $C$: + $$C = P_{amount} \cdot \frac{R_{level}}{100}$$ + *Ahol $P$ a befizetett összeg, $R$ pedig az aktuális szint (10, 5 vagy 2) értéke.* + +## 3. API Végpontok (Baseline v1) +- `POST /api/v1/auth/register`: Komplett onboarding folyamat. +- `POST /api/v1/auth/invite/send`: Meghívó generálása és küldése. +- `GET /api/v1/auth/invite/verify/{token}`: Token ellenőrzése regisztráció előtt. + +## 4. Jelszó Helyreállítási Protokoll (Recovery) +A rendszer két szintű helyreállítást biztosít: + +### A) Standard (Email alapú) +- `POST /api/v1/auth/forgot-password` -> Email kiküldése ideiglenes tokennel. + +### B) Szigorú (Banki szintű / KYC alapú) +- **Végpont:** `POST /api/v1/auth/recover-identity` +- **Kötelező adatok:** Vezetéknév, Keresztnév, Anyja neve, Személyi igazolvány száma. +- **Logika:** 1. A rendszer azonosítja a `Person` rekordot. + 2. Ha sikeres, a rendszer kiküld egy visszaállító linket a Person-höz tartozó **elsődleges telefonszámra (SMS)** vagy a **legutolsó aktív Email címre**. + 3. Sikeres helyreállítás után a felhasználónak kötelezően jelszót kell cserélnie. \ No newline at end of file diff --git a/docs/V01_gemini/10_Billing_Credits_Subscriptions.md b/docs/V01_gemini/10_Billing_Credits_Subscriptions.md index c930070..3171424 100644 --- a/docs/V01_gemini/10_Billing_Credits_Subscriptions.md +++ b/docs/V01_gemini/10_Billing_Credits_Subscriptions.md @@ -1,25 +1,35 @@ -(Az Üzleti Modell - A legfontosabb frissítés.) -# 💰 BILLING, CREDITS & SUBSCRIPTIONS +# 💰 BILLING, CREDITS AND MULTI-CURRENCY (v1.0) -## 1. Előfizetési Csomagok (SaaS) +## 1. Regionális és Valuta Logika (EU Scope) +A rendszer támogatja a többnyelvű és többvalutás elszámolást. Minden pénzügyi tranzakció két értéket tárol: +1. **Local Cost:** Helyi pénznemben rögzített összeg (pl. 45.000 Ft). +2. **Standard Cost (EUR):** A rögzítéskori középárfolyamon átszámított euró érték. -| Csomag | Ár (Havi) | Jármű | User | Funkciók | -| :--- | :--- | :--- | :--- | :--- | -| **FREE** | 0 Ft | 1 db | 1 | Geo Keresés (Sugár), Reklám, Nincs Export. | -| **PREMIUM** | ~1.490 Ft | 3 db | 1 | Útvonal Keresés, Nincs Reklám, Excel Export, Dokumentum Tár. | -| **PREMIUM+** | ~2.990 Ft | 5 db | 4 (Család) | Családi megosztás, Trust Score részletek. | -| **VIP** | Egyedi | 10+ | 5+ | Flotta funkciók, API, Sofőr App. | +**Átszámítási képlet:** +$$Cost_{EUR} = Cost_{Local} \cdot ExchangeRate$$ -## 2. A "Free -> Premium" Szabály (Q10 Solution) -- A Free időszakban rögzített adatok **láthatóak maradnak**, de **nem képezik részét** a Prémium Elemzéseknek (TCO, Trendek). -- **Feloldás:** Visszamenőleges elemzéshez "Retroaktív Csomag" vagy folyamatos előfizetés szükséges. +## 2. Előfizetési Csomagok (Adminból állítható) +A csomagok limiteit (járműszám, funkciók) a `system_settings` tábla szabályozza. -## 3. Kredit Ökonómia (Coin) -- **Szerzés:** Adatfeltöltés (50 Coin), Meghívás (200 Coin), Validálás (5 Coin). -- **Költés:** Prémium előfizetés vásárlása, Skin-ek, Extra lekérdezések. -- **Kifizetés:** Nincs automatikus kifizetés. Nagy mennyiség esetén (pl. Üzletkötő) egyedi szerződés (Megbízási/Számlás) alapján, vagy jövőben Blokklánc (Stablecoin). +| Csomag | Jármű Limit | Kiemelt funkciók | +| :--- | :--- | :--- | +| **FREE** | 1 db | Csak GEO keresés, alap költséglog, nincs dokumentum/export. | +| **PREMIUM** | 3 db | Teljes dokumentum/fotó tár, útvonal alapú kereső, export. | +| **PREMIUM+** | 5 db | 5 felhasználó, flotta-szintű statisztika, TCO elemzés. | +| **VIP** | 10 db + | Bővíthető slotok, egyedi szerviz partnerek kezelése. | -## 4. Befizetési Technológiák -- **Stripe:** Nemzetközi kártyás fizetés. -- **Barion / SimplePay:** Magyar specifikus fizetés. -- **Coin Pack:** Mikrotanzakciók (pl. 500 Coin = 1000 Ft). \ No newline at end of file +## 3. Evidence & Trust Engine (Bizonyíték kezelés) +A rendszer csak azokat a szerviz eseményeket tekinti **hitelesnek (Verified)**, amelyekhez tartozik: +- **Fotó:** Kilométeróra állásról és munkalapról. +- **Digitális számla:** Feltöltött PDF vagy kép. +- **GPS Check-in:** Igazolás, hogy a felhasználó valóban a szerviznél tartózkodott. + +## 4. Szerviz Minősítési Rendszer +- Csak érvényes szerviz esemény után adható értékelés. +- **Fellebbezés:** A szolgáltató kérheti a valótlan/troll vélemény felülvizsgálatát. +- **Validátorok:** Magas rangú felhasználók pontokért/kreditért ellenőrizhetik a vitatott bejegyzéseket. + +## 5. Lejárat és Helyreállítás +- **Grace Period (30 nap):** Csak rögzítés lehetséges, statisztika/lekérdezés zárolva. +- **Zárolás (60 nap):** A fiók írásvédetté válik. +- **Helyreállítás:** 6 hónapon belül visszamenőleges befizetéssel minden funkció (és a Free korszak adatai) aktiválódik. \ No newline at end of file diff --git a/docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md b/docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..68fb2d2 --- /dev/null +++ b/docs/V01_gemini/16_TESTING_AND_DEPLOYMENT_GUIDE.md @@ -0,0 +1,22 @@ +# 🧪 TESZTELÉSI ÉS ÉLESÍTÉSI ÚTMUTATÓ (v1.0) + +## 1. Előkészületek a távoli teszteléshez +Mielőtt elindítanád a teszteket, győződj meg róla, hogy a háttérfolyamatok frissültek: +1. A `.env` fájl mentve van a helyes jelszavakkal. +2. A konténerek újraépítése és indítása: + `docker compose up -d --build` (Ez kényszeríti a Python kódot az új verzióra). +3. Ellenőrizd a logokat: `docker logs -f service_finder_api` (Itt látod, ha hiba van induláskor). + +## 2. Tesztelési Forgatókönyvek (End-to-End) + +### A) Új Regisztráció Teszt (Clean Registration) +- **Endpoint:** `POST /api/v1/auth/register` +- **Adat (JSON):** +```json +{ + "email": "teszt.felhasznalo@profibot.hu", + "password": "nagyonerospassword123", + "first_name": "János", + "last_name": "Teszt", + "region_code": "HU" +} \ No newline at end of file diff --git a/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc b/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5bfef7299f97e126bb174a03b6a4a2205159144 GIT binary patch literal 104988 zcmdSC30xdUb}$Y@0|MO$30+877eYc8x)C}!B@jXqr#Ph1Fg*+d!wi}O1d@E@*tIv= zjTAev6gjc9c4D+vvM6?96gx4_Ix)_AzwJpk!Q1Q2y-A#NleKf~m+#B}RdrW)^-y#4 z2(;gy-;bJ^?&?>s-u+%xcYkyK{LCcyd-r#K()H1*%gwe%zDRw%kT1f@~SI}^SU~12a6p1&Z3>2yOc8ye{tSk zn}aXfcW~#=+dt9bXW;yt^!AK1HR-=3n( z%uiu9Nd*>pPJ`o8(BZK=l&tGC9#> zA>Z^*KKXU^d~&RwmbiPS#NCr)_9OzRqe+irI4x zv!{7HGYA|$GXkFGVa}ZenDb@<=KNWJxj=`>FTB6#btL!j7n7fvMQo=vVYjLyPyDYK_}DlH>$_?!rMnuobOCQR0ha>!MNxtkw- z1%bn_oUs|LiV2f7qm|^UN1V}W0*B9yFeBFJx#TLt=&YV=;_kVY+0#6a@(3LM2@^AV zf?Q>ok@?Zr5jgz%2&0>axgjP@7UkEIs|+(@_1qYD&rQsp=4X^o;P9IxP>F@-W^z@` z85J;ln#Xesfx~Y#f#+6ol>txlqZbl5{5BJ#ZzES3MrYA;d)z&D#NBge+&yBUmjs}^Drx7 z!emjtoLu#YGpZzT_^KJ3(eYV;c_JoE7L}^VRR$`tdRE8XvxeEzJl{_eIDGAlQU25{ zz^sc2lSR*3a#c*qpJw(nPtP+14u3WRJz033C07~nG(UPhfx|aM7~MR~#+Wc!Gio4L zJ>ra-2pqmS!i?r9wFkPTYHzHnRf`UjKj%#5Tlu#8?XRaQ5DfU@Vl53hDON?SgMxkj z{sm@b#5`1M98gjHG3<}O$T#ygz7yt%=Gg}Gw7;INbTZ6yLD+@x6RzZ$6}?4_z7D=C zu!J1%$tK_QPu_XIo6I4QLyq?>C*Sl>-o^Luy}TQ6?IS$lTPf%(^}ggA?;)`7dtX;M zZkV5W8R;hwdB=>)$R!H)%*lu^4ybGyae$2YW6DURYn%B%%xleCouD?Gff>{LK^qZQzrWUw28i7FwysmCi*^W zqVIFezH4-?Bj5NhQ63$5=f8$ZUef1Vn9lg?-IP2=}$%=>w2?9F6E{gaGM^S2>~o2 z>WQ2qv16q#@jqiSR{9eEOH2u5dBrdBw@vhY$wc2@HqrN2n0;BCewn{RaeBMXJMxY2 zkm?o9lU%=lm*hr<7}R{8^Hq({FUR0Biw`gJuP|xA?)$2VzQ4-sYd*gFHEn*cMU3yv zLmi0&Dm!X?jenWH2bvK*ACGkH*Z9|CUdy8WYy5pCJ=uMK-9+EtFwytxCi;#t`?7S) z#y_C6hEaPUx&^V{ZxUZ;&>A+>7!?fyl^S~|(PsUVe}jLM|1JKv`M2(m5sl@qQ`fM( z^FtzC_kV|J&Dr?AO+hWvF(BUvzA;CCM;rZj3C3|y>-VUt4(xvm{{zqTE`dwo89jPp z+c@^@$0?|f?nxurSMxdRdt}zUjsN`!JEhD+{e}*8#c zY5jnLZ7y@S@n7P{6SAW=eu8P2Sa#ILf5k-K4^8y_BNKhUX`=5(Ci;Gv*_Wl8-{N1V zbaP+0Wk^aQmWk>WJ%eZ41@m6~e`yl*tr?6BzC9~Yg*c$HqlRxmB|mnBHqy26L>!T> zweg{tT4vt%{jo*~F@o(gFA0(klb?*RYQxTRME+TrW2Bm}c})ImKRs~{@?x*~26z>cX zeEiwd_xZn}pl*rF=NCrq6*Fh~TOzX&GA8qp>)#QGJn#o(D{}qIyfyfgf*r}mv$f>= zd^{_)Eb0BeiR#+-d8!IFbg|&?hZi>WJAF3Zk>|H}JNj(f)z>6cRnya5N?Fl7sBhnBl8TX|c+W_3FT9B!-K>*;bi{cx|C zJl}8x-M1C6bpmXUtnkd38;-wFLzh zA1YbMi}btHtb;I9hrF5~?Q=L`jyAW|?+6539)(GdJTAaiAq zB>fzkx<)rVh#v78hS7bBKj-^Kx3ygTwaLH6DWdqAbc(h73f1&)pp99&+B%4 zifYXFlI1kF!{ZEe%c=M>Ila&3v4Pc=(|Df?qyQoUOr#-n%{`0EA%E`chHHPcG+*hG zUtCQ0;pF*_%Pv3ibl5HUdXaye;|aRq25h%O&e6^y)`NfG9bLAdJD{7NFnkfY*M=V> zzg0@ftH}Z4Ah5iUv+3$DIg@k)mxZ}j6n%|4J8(UUc?Qo2-@v4I0~^6=dOe+9n~%5JyKQjSz0cY^ zC}(%t0?=v5YPa>vQ=K*9Y zKL9O;rO>5V{7y zX58a^PXS_zkS^9lQXE?@?s(z#5F-Q=XcnA@XUgM&FR0FqNC^=K~&U2%V`%P ze3o|GJUql+T6m9)40wOgH9Y`9h85VGrA7;`Vd#&ZlfZ*45vV=~pbZa7AZP(_MO-MO zje9_(qe?fqgt$C%#as#oWCKdx0n(-S1Nrt0coAp%ZD5X;^xJ%3`7Z@QB0&LMAcL3X zxx7P*r{pY5YCDz)c-TX~&E=Dq_&|8U?zq6N+mS^?WM7CPc$i*4=;a_x=$=KPCl<)m z(uz~nbu!n~P+2dhL(^PcUMbJ7s;#VSw4N%hD?JX!=OFWe4PdhGDlp(jEAr71WcWa9 zVI+qy4?@8q7@C$R%q-tCh+~<6EJWfC5ED7I3t~wJ;VQyH?W1-8RRg;`C*bOH40}Be zIYW0#E0GPrwoD0FkkcD*z>_T8LqFQNd9ywU%2BFHYa1#H@=EI}@;2+BarS~U7YLOV zVBlbN?2v?`5rFNQH$)wgt5Z+lvE@|dV`q(FQp5Y*X1_COs}B0cQVNIw+ctyX_P*sVA^r0?4F^0c$|pwEY5h;T|S0`3xc zcy96nBOu@xvKk2Ktm!}?0SWp8UP!#KOI_yWy?vkqJ`izu91B=~h+*u#@}e#fWJCDM{`E+^{)TZOcO35xjyQB2{qGFdgNz8nEGLcCOmV9qjYS^B~EBso1^v zm&I?B)0<1{%TJWn`_MfMGFMsGbV}v|j>`dgxe81P6yX|em`ocF&R>C%R<3+` z>@h&w`-kN8pi)WlEr;jdECp!(cNZrmJ)QJfT2gBI7f(HNO32zGaa%v(($J6V+axZJ ze9xD-mE?P&#I04ozp(Db4L3FjIlIKHqOqqYxznW24vAYwz5~qMu>SkStOG)2+a%W> zcA{9!IwYJpKgnGPJF!p9+AmbJPI7HwC-#V0dxf%=N$y^RPdxX;ZNveJz- z_Nqzlco@|c^*uMqwQ9pfq5)+_2(l$&*5QdA({&%I(D=eh?hL_*HapaJ1N8~eonqFm zG0!A-DhySuGEYu&bz#Sm%^JU;3o}hs<^a(+N07ikfu`Zw9h=rd4wB^YL#oj|{mkhX z%3iFvQ6a3@A!hF!-8#;V4NN$Mj?PKW4l|kJmW8-wFZf;z+z1G(_lr3P#URvt-czNz8`30{H z-W?QnREhb=KhNM&tCK&=M6QUM%w92jpKzdhqGDp7&}J37fnnZhC?6JcjtIxj3aySw zu4~%aVln5CaJWur)<~gD36LJ$Kh`>Sa$>{8GQsK)2Cq$WPvXn<4u{59k1r6;HVJ2% zg)TQZlc$m^u`{V6&V;aXvzS#d3g}g;WFYJ)60>)YUKr=cn}x_im{mLu+J^j{Q3MW(ImKhAg$C;+cM+ddWnkfG&RFqS;Y9XC8Zc29 z0CRPX41M&@aSF|&!Uh_zqjzd0ZkOJ*M&fqpzl`Uq)DQ&C{pg)KiQA2OqJX5}rTUj! zZ?)bTdUM@d`495nC>VE0xo|PjV6DTIH_P6teo+0!$%&0p?ioq*uv&+!Z`Qxn`k?iV z_KCex?pcYe4t| zn$L%t&kGlN#b&oKcty&+Dk;36uDFAcbUtvt;gZhOPn~HAooNxyIm9zv!lj^;J0Nj` zXxgA`*Q;*^kir6SZ?}K4H0Q;(8*R^beEh_wk8__udbxg@T5c1ww~uCwRgCSM*eIL> zt7rvKQ=BT(uF-n)U2yv*Ilnrruw;|4 z`KZJlqkkWkxFhuM5{Wwuzm3{)M9et~A(_wyO6#0UNb^g_oclPhq?N^Ew3Y{HTacRae8fSHt#?Nfa9qgUuG;L|9blV3Pqn13RT2Yy`ew6s7SX-sM$65X zJ6&MV*G>#eOWS}2nl-Nx^G_nFIi*|k>&I_)XxD$de6`wlWKhiCk63q5W1Z47e=2uZ zD0kP`9x3-28m|3D`}60gu^IZdBdFnGOG|@h28A3Hp^fJNEjyAaqU(lI$U%|pSpFne zhNwhcxmC<69LrJTN+1U@?c2rd9bgjoj}=T*085Frq&@96F=zW|`j{Hn5f0x;A6YTl zK6+}rVtgNxg-e5z++~$D*Y^vni&U$J)MNimFmX8M#=nHs5O!_X`rbT6Y100iC)K=0 zxe&}Oj_;LU`j_B$WIR(*4x4=XK`7Sz+T|iQ9*IYlx|WxJ8X6sN*=n$(-)WaqQ8f z2&fS!6#R^}O@RD$;AzmZi458JO3KaCC%20^J4RQImyn67AwcFxopAV+P*N8$Cema) zQz$GM8y<5{G)`1O+yflDl3a>8Rz`E|PUgr-p}0;scv`4$7S6T^&OYdR%u`~{I6U^mTqsn{*fkd zk(jf4v~?VG0tNpX)iOr*kG77U9N#d$OlUa|USg6PAS9UP8Yj7}zGYC%F+26EPjVVa1EmW!6)~htQDxKWm z!rZLKdaO+{1)qiKoF5COtPNio@Ko?je(wEw@!s-8Jv1kaP+R}x^~VS&!_&SO!j`y2 z&SXMA!wo`=Td-ol)*t5!IN#4AH|QInF4J)#-(ZGID1I;=CZpY2pvxEIhAYyGPobc( zq$it5&;6y$8?)FrE~KC`Hj!(ir`=!1{b!l#I38buv|^y!MNds&*j(LUrR=#`bL!ujH`LO5)0S7 znWboq*=P^rjbk=!+p|21?RgwmEYjtPe2Za&^alqo=hyQaqS!a{us0IB!7t}G@%b|f zdou;QfZq}Y_69-;{WAtlvktzy-*N(vfgx z*|qhQUa;)iF6#Uyor>fev0Mq-t5~BKQKLuoBMDi{SflTzMt`)qo7bN`nvAdK_wq$i zWSnhV)`K_rg2u7uSz_Ai z&g>QDIjcYLw*;cfBT+bO9=7(dU%rGt#+S}0Z2b|uC1BCZqrhfGZzX&wrA^UpkJ01S zs2YF2f*FakY}^8BWR{Jqq|QH@o+mPT6*YRHu94*1BU;#4b3aavzMYJzf6nsgC#V=N zMwXkPhlw@+>L2U;YpBs5zvP{yMvtm%F}*9>HZO=`o6Xxc?ZL)iHBa$%QS6WTY+QR3 zGQf_Mjhm17^amp^!1zXr%gjU89-a*N*7FV6AJ0^r+Neby3-~6Watb_#xP@tJS#};~ zN=enDXXz;s>^yty25Rh`x)meeh&3}|bS;Df58lUk1B%*BzdhS@9bW)?QBO0!M^4%tCSuP^K zvqY&L3fD)Y6x-9PH27HQK44-t=A%dbA?tk+ zqeoV}(#Kz>xUf&RUE~|Fpj5AD)+17^!qSUV6ik+03{mH2#!sTxrK-6-Sz$0#R1?8+ zc2-Ge1JhxOo-A*5#e}z_qmS9Z^(uvH;?c)7O6O(+OB3tPaYP_&FL^3T71g|*cv7PX zz%{Yvdy0QtE1w65<pkVuFAIm>hZSt|A`(MKiU_%HKc;eQSuoO(mAQ_K<+o_=^#mM>FS zGHeH(YsbM&wp}Qg(fIR@MiGD;A^tRP5$;k@zZ%6NuxI#66w7D+3iGz|RSo-Bz*eez zJo#U}|7*;hf#%`AM#0xs%_G7$pHt|sWdKS>;3=#2@Cx4+!wcWz;X%m!eSrGwByW!) z@xMXEY_V1hzE0?Jf0RjSmX+T`jnA_352*9e_Poa4TgjHZO;Kdkykz_)p#bcri0-A$ z!+wK;eTsh&B?2%nac@$vBT1ZjEAm?uRD^G&xZ((j67^61w<*4}DGY|P~3i4Y3InvHHmQ24Bg9o!A({b7(QTV zV(QB86S)J3svo5wRv3WcAN;}?IBG9naV7!p-zPgiUsvx@GSu&YJweED~xY9LEb z{*am(%U02P65p;vR)(*?cm>&BZ5<=V*dFS9blH#J_QtQH@xz>;@QgwuV-`bQ!=n3# z)cNhxC64G5)r&><2s;?4t5{?I5p{m1=>AR0KFt0)@kg3Q7&2^XUaWb4i#i`|ozM{f zQrECx&fE%{{_2@XbE=vyC;Yc#SSIZyF-%*>vdU-&0lcIo23+lxD@?1*f;$0l#h7p< z!r8dtqMS6g7nzI-_ZWk+u_7+*wJc1^GQ9}zf%{gY$8l2xPt}vCGWWWg;}|>%%iez+ zLtbfnA5Hho#QNW%#@xi9q;ahx{&-)G0r!`veA-OR|6K}hbUP3~uAfjaqu6s(KH_^6 zOcq_Q80OFRXNgF*_PUq2CHp(P97C@EEKIJ+PKTL^kp7%N=Rf9Uum_Q{WAm}sUr><0 zGoz6Il7bv9uVQKWUr~~wS_oS2_`jykvn1#H2FYPdIZJZ>ItG{jhRjKq91^p~v|N8n z;mn%TY*?-X)R-(f{aqY%V&VE}47f&%$ktpI+`peCxc@-GT}Mlj@}0lJ|D%52vnd<) zPcb0>bHb4S<*`PbKOmBgXCH5d^RJH;$A3!*$2hB16dzfc2)&z$dWr^h70aH)im`r3 zW<=SOxr$82v?s@?v01t_8}{TgYD|VbAz2;B5ZmUm@Z2#AFh8eYMvDT?WJ3Q=!Hhol z`0d?)Q2GJ&fB|D^Cd!LW7zb^k|1>P%U>|2<1^|C^#g&2)UJQS+vsB@!3u$r;qu zEZO@%)cKi`lbOqyBRsi>!kIOvxbH!t@9K*I^Z!!k8M4pQD}QEm>VIYj?)7AzuA#Cp zjwiQZP!}N6&qInLGt|r&{siDy3|bhc$A#)*RuA?fKF;?D;YxIdS`nxa$0XLrzr4iRtEY3MTOsb7sg>tRS!f_pGQ| z#Fq4xk6+RO(^b@{(WR1YBN;jgP*=|i)ZFN@4!$|wXNAUIqhrr&P5c;`$~hlb?S?(8 zwG_7!k9A{Lj@YBq&p(`@SQp84+|lk6L~rA@U`W0(^mTFA$MsR6CuYSqP%w!VQ?n04 zB*3;a8!2uxIP&9&?h?(bffMvz*-eTir`yQ8)1y-XAm>v(X3jEhrYy+JRDsxjoyFNW zt&ADV1$3hCa+o69nS~E;cXA7t6EQL#AS-~~}Gb}4$*@8qOckM}@z~zkz zL^O28nC)}I?1Du{$(Um1T*n|=akJQgJGoQtRLe~7*pDt?$Vx55)U-zuBV4fK2^ zpmkLoQTB1d4`vL1i~&2wiZzUTf`Un+?70f0IHK%oO41&a?=w7kcIG0n8fqpp2bF#% zC(BDG>hESj4?q@bXN3l*D4Wa3d}p+t5m~?B38OLV&-!yifAxCu{WIjg2#P9+>6rF> zPu>5&u2Lcn9ivx(ACr!OEYQ#1oH>1%rT7oS1ymm;~1yCT+1JRd)^+TKFoQraHBM?FVz?=fk+L1GwQio}a1D<-YfOlA%$eV_ZI zd6R_pLO+;hr_1Kt~J;P_+lDCnMtqSm%khn9MOQP>>nY z#m5s_CFaeo1mhU`ok%U}B7qH*ITy7Sl?Zg30Xljbc%(ZI*!2SX3D1wqHpSR6@d!0_ zI~A9%nVvgQ@|8FJEU}4+?NkKr(oe2_++O-ovxmg=#zCY{*Bgq)iPeU?2+V|QVDxjR z|KF5pGqu||S92)3v#oMBp{q%&9BZA5+NT*4hX_ycrmkb~3pA^T>cR313HoP%+)F{8 zxtz#NL7iEA>8Cc&AY}e=RomJ8jWed9sLGacZ-O%3PeG0%1ynD__;KvmP0Y$(BD0!7 zE9)aL!>sHA#V^KD!|+qoU~qLdR^vfm1JtN9DE|OCNI{NXZ7^fK2Pmj4e5vQXgIzWQ zc`ycK!b2SYo=NJ#U@TjriBPpE4o6k9)0q z7Aij=4YRqz{4_;PmZyrR`ogjyk5dX+Yen(y+J1)EQjo>z_YNf{-#5+;~uTJs?`j8g{XknyWrXFF;}<0MDcj0Y}U_EFjbpHafMaW zI}{VopCvHEvd#Ey;g=~K9+xeg9UuM`Y9=!z;~+CX7u{}|*pU%C4`R?CFt|?5Yg5v+ zKj?A8@GKiM8R^ecu%qYZY^ZEK`wG^x=i|_`7l`~aV*>{C_?|2ZH2H<7kQ1}~+8ZFK zsG7AmC6SC%dj|yI{^bNR&YKh`8FDlmRijytX&BO;VDv%1fg(Cowwxy>FTX^|V0;!o z_I^80djaV0Xd}KVDXsb5HS4}O8Oc6eT0=y`dt?@=T+{+ zz&D=QDKVM6OXP>LVJe>tvcs-xeU-xd@p1PRYSfwI!K)P16N>iGEuY}iflZe6}=<{wC3gcrz3vk8#4*-u%LZeQY{4K!0N0>Gsc*-G@4eJi$&wV7c72zGYeif z);kb^n}5K!^B3Ur4|)7PMELdr{7-y+hJogcSt9xfkt}B>&*bwWjR=|yK5x8m{k-W8GrmT||2fg=jEEtwHBA;3emr+4Fy1_5B4P|tv|!C?7It66!2#+% ztcVtfh3zc84+1d$V~S2QC^GLR%;j>r>;ia=6$lrHR>6`b|+J zH;#X${7)#XS&}ddatn4`waZCKJ<(zm6FZ!FI@_Khn85iMyW9xp&18A8YELm?K8ior z-qtmPwV98J&((eT>n!<8BwGItfepO)lLY)N{YI|GB;9~%D`hWAXiK4dhaTUB|4-om zd+`6KKsQb~W5!0qm48O*5zChTonZ%s8PERb6y)fcW!z9_EeoEBw~n!7<8j^Nl$h23 z3!)ji)=@lTM9}1e$Dj#D9CpCXIpICl-1736e zyExWdKP8gQ^xQ1iMHu$q#{v5vqQXwhKK&yF)1r7ciYF}lv;i{JSe3_rB2dHZlNxV7 z@=EbP6ZjxQ-;ZiP5{YL1#Q=Q+yd}UA(fYBge?OpT$k37IVO7<5qc4O!!nm^XgAI71bpAo5vTl3?+hvoCAkQ3AUf2Ux^r}qo7 zG{n;U{~&O~G@n)3W9!a;QWD3I$yw1I`WdFtr9A#PdN#7>Hi|Zz6l+bli9TJN-!V&Y zLi2zAca3gVOvPgU-?cvJk^U)=K(m0J~j2HDacXAk2ouarHQ-J|6fGEBCaDG zn@*89D*f8K3H3Z>#`bmnZ-O(>{6C|5YqmxBZ;IXwiI^3Ou$vl_p=m_MfX8uL1eiVj z-oqJ!YT`aLGnmqE9xP@^)Ed2JK~ajWXa7U^k2NCRrxRNY?-ES>$xE~ItZ)mTOlGBP z3`Gf6R_= z*R~m&7`EDG267q|hn7s&9Njo@K^C$}x%Z^2>H<{V}Y2lxsMvVs5OwMZI*Ubvl^%PWw z_7Y1D)`p$69%I$~X7V(^c`KDuteLJ4 z6s_{6pCuZW@cp>gDfo{S_&A=3F9lf_~=Aa&=;+T@@kqV4k4F8AV zzl6zEMvVvh9wuDF`V;@E(-Deth10Piag!OX1Z<8{P-jXn<6K>*MrKfokG(#|@K7+L z1F$p3!XJydGseP~5>7C=$A-Qi@4JcpEt zuJ8_eoIV@x$n)E~9euX#zJG^4zTbeR(I-GVoa*roZZ4=QNb$Xj&fG)I)2Km`ROjnL zUwfb_u=u*s_g>We2sM4E`DxTRQ1f5VK&Nc<9ryeX^zA=U^G(!z12wOq=55q`4oyL_ zJlEzAI4%dQfuVi}T&)lP8Jz!gA!?A9FR;iN{XU1?<#%~Ka;o2Mb31%LgcH73Q1dUS z`95m?6*d2i8arzK4K@FSnrBh-1JwK@H1bjx@9+d%T`s#V03fZNU|*-hSCH&;qAve{ zy3BXG><*9LVeNO@0uJBbqZ6N^=I>DROQ`u<)SN*LzU*(%x2w>=#B%$+{(#fx@LzHR zaJSoU^>_nT-sKOtJobR^uTj@qsQEM0{3UAs3N?R$ng-OogPK1_%@6Q?)chf8K0wU`YCc5GAED;=Q8R{`_fRvAns-t2d(eEE0?lyBuI)R0zl)AN zM9o{M8AZ+Spys!s8J?Tpem=jrxX17Hbozb^j(m!xDTvOk&hK}4c$dcs9qipUpUn!|s4XgWb3F$_hFCOl4g~b=`3}Lq9gBzOtsW9GzV>U~{{8ERgdmpjhtRy+E89vQ3+B!$Lrx8ZU>P1C(-GjK+Pa(E~91$HN&X6f|>wouA$~h z)LcT18#OM}U=nqrZx^AFGl79f@p>IYa$3LF%O6w}}jE74O=k1l(>oXW|IR^b!Lmw-67JKh- zHtE#w^A3P@RcN0_j`|$RrQ|G-0W)DZOYecmgE_WIKc-xz|5iz;FmSbgTYVuTomaV8Z*1k0XCnv+5$*GeXWT5Dd_49x6R|U+P%C(P6cfZIxsK$gPm@dzuUpf z96B#A4uA;w!EJzqE0PRCc*#l~MPRL{udf}+P8({@p{4~5H5UW|c{=2lKFvc%mVm?S z>vY+z!j_QOoQKI^5We z`-dw55DyR?Zs>?iD?sXZ1bkk%_26FpkYm8wg;W+d3xh;@tCKNhz~zxM z!R)(ip!lFlsdlg1>yzjA2YmnsyfxTQhrB@hiM8U>y)ad9?zy0>@*-k3yr87Ww8?1@ zf%t5Gd4YDu=R~j<23$Ue)$ba1Si5{)P@@I<0T2)v760bJv15r?X%G4Xp!h_>z^H-h zgTdjU;; z6D_Y|oIrAj73VCv{xoXp5Rf^YHs0w#q|E^Gd9Z>FXW)Yv%@r6A<+qjeRZv_`pTggX z6<YJ}Jv-n=H=d}hmzO#&Lu3htgJ1)E5BA9%wj=Y1-$db?ynt8?&@C|i!(gTr zMMSEZNAy62=LfCEXF==~jxQw#v>uq@#}O@-5R;&B2nj31HDKki2rSaZ2FruuU~(4W zIjraajm{cvFa%>7G7#^hkjA$YSwzib;PKSZs|umD5QJ2-KvCe-`!qS+Q_ zJi|nb>xQII=K8(9fQ;i)#bKkQWe|mi;J~`&xjbkp1P87@S3pkL4NhYTvGdBApx@?n z$k|4vRaH4V=y6>NLe8oNVK^l49mDg4Fu?6VF(J4mAY-~3O2~744krpWun&W9!Q%|# z0o)EJSV7)v526@S6^o+)%69~sPcu-Hh9EBp-yvADW2oM_24aS1dMa0&-Fyw?lqwr=!d3gQyttRa`g0zG8!+Hxxqj!V8t` zp&(73(+9D&8xs!lc!+{<`~@D+I|v@UcTmnibVo8^lQXoK6Wq@bgiMfifEn+%p}==| zF|mLcqT-XuArc!|$!K8~T04&kwFgamH^L57rON?HrxHux9}XPPEJB%<%a4P0B^_N# z9qD)Ypp+Be1V*&35psZDm7wq6+hN z77Q=Zj)FS@nL!7A+YwHwr(5d4xu;dsS2x2axDlL#%X6V4ng-+|YU<|j_1Qeg9_ATb zQXiyS5Xm9$r77AI%pL|^Tfyw2Xp?H1aewIMLk7TCh_KCeTnf7Sk(IT&Kyg%4yA_?v zR3sFWcb-}^3D|la9(g_#6YVGy$6>Ub)2Y-K@gZ;@n!DSAz^qhiCQvGY0NiIRgFsmW zm0!p?%-8~iTRysSUVY{9>IMkN8mmv&L8MmN(9m+azQS5x*-+Ui&plporrrugE)=$9 zV3(x&?O*VsZcRPbHlFEb8zem zHX`KLqh=kN!2-}?;;@i;n};*OU_bI}{P05RES|?&EGEEUh_;Xcpe|KC6y3ofgGw-{ zUWzeY0mTR0g$9H>_?|$xX~7~a7l6=rgO@TuTt;T9pT|;?hi11HK~QUP_8}#TfLfdq zMc|)MEbR`0UMs~?TrqJ3t%`=p%M|s8NW)+$WzYF`O415U|+Dr({A>+qMp9%>_Wa$tZ0zDv&L@kO& zA=pP5JIDzNoG^qiW4WBw0~2i(LV7i7Rzd@u*J@396<`m2E8r+p9G|d2kIksrgqr2( zf@P>#3XRMu!CI#+VDCoKq+r%LBrhHC2B9DamJ8$BIiyI-eC;q6h{f7Z1P9Y{2?9G0 zyCiir1Z)q)_iC{<0}4#Nij{{r1?sK9St^Bu7=%7XgTc_V5ax@~5NV1L!=W68EvG>h zrrQg1=(i2Iy*4P=+JY#R#3sxjhYP6_n3jtW#LNM2z@emMau)g?#tM91vo@+GXaO?1 zz_$>=$x-7_JT7hb7tts1xC>B!rF=|O1DLSbXvPs=()?a1OhMG$1=IHWh9Dg5g7Nxn zzFrW*MfwFwKdAMq9YI~fPlDojAQpC`(lE#%tlYs=hqLr<*mod%whdE_V5WD_zoD+0 zU)vg?yWub~g{urlb%=;A1^GU_;xGrYN?!_UGEjq(1m6<;4K=7@=}SW2{tx>0->9*m zp9@j505#dr$f-3Ar|ZffkB9P{oL<*-suGF=zWLAYVC#e^Ap4BZ1^gH~=@=@9TmteG@P=}B6W6zNHAUWvIzP zr!#o6QtN92bw_CWzYZ0br;|QQOG-`u;;Cm&T`zd4{^izNt#_`z**3MeI<&W1XlRl4 zo|9a?Q?CAyt6#WuS#%BkAStQFa@qnPZI%m`&yteXSW%mL(eeqGHdAn>xMd-3+4aE9 z){%j+j!CWtU{7%?Lfnd%*1f#p)&^niUUB8Vv5fKk6Ri^`g|-Vqt5xDIsu;Ym?!^r^ zHV8Re#jL`y6%+d=xq5Ww^fRZgSKqw)N$%R0+itbp?)YSB&Wo)#TAy$K_=%05RN?n%GrQ+mV&qG0@ob?JEMBObf zb-wJnIWa)GW2v~ZY`jJ| z(=J@_3FrO7z_1X!B5_xzbv!JtJTjg+Q74@56FNM?rJ&G1AaR4x5ApbgvKK3ER0u1! zh}m05+Jxe>!ohl>{o*8N3+qrQW^WrA9y=`@suh~rCb{;o^V`Mj9iuCSBlW`J2BE_? z$#tR*zyN^ z3xcap+Tf8m@3dZ?cRJtgdD!!|TWGTiJsxR;SK|7imr+%Yi7QKm@&=*FCAf!#-eF+h zlgVX>i`SCNG5;!P{t3C;M!UvOOme3%q_rV#?W>m8xVxONu}sV>7b=^Ca}L4TC$x9~ z#A!>T<#SN~CM;VTF>mII+4(}j!Lh2b0}~aK+*u4uQEDN#Xl&oa>PfEIJZ; zp{iXt&kIhU&=rOE72?WD;dqB|p-R%8U)ZnQn$@$nOzK2J|c-Tc`cVqf!LDth_g zt%G;k-(2_BmIqtjD4cKz7doX?c8TMG64-WmLd&lx_=EKal=LwCi0^cpIa!FiI zSg$)BZx+2({Gj-al8K;TwM(mbiF1VY5jGwQlipRT^lB!eWu$qmWRg1pR~lt?r?_(0 z=>Dra&6alb(Pinu17jbS>YhIE zlkfQ69ega?f8^V{3r9-(hoa9;szp8_ilA5SJ%U^-Rm=x%(~_uA0iAz@eL z`@>VmTSCWMB))5k?+x+2BHt$+_W*~ES;{Q`fj*8~{#E$^SSKt^$fqAuqJ%DR=5TPp zGK>b;d<744l`%}!N0x7AewZn=bx0@9OP>BIPcY;O!li@B7@jv7M;&p)JMqz$shYM> zO`GI&O*uUwr$==5OEs4Oi*hj0fOfg12BD2UV3?B@7ec!n*ayFcEl;73nv}W}g!XCF zrk+Vr<%3v@y<*lrp&azxG0AnIbD*C>cK%2%QUnNhZ?_}$X}Qtze5+A6jUjRUTWt^8 z-sq6(8>i}9L-nmf+j$W}6OSJ-cuL{Sno3N#Dnhsf0%{yeuepff*uvom`3K)Adbjvt zv2dndXls`abx3{QslGs{FCYXji+zUBJSwC+SKcgptL8z?8@1BehN-jXLTArG04<)q zDD)$4mMVU6&2fy!iEuo^69l2VPq=(l%DpCWPg3fDWA}5DTvZrDm{EB=QFPrib=MEx z1WhbN5b-Zz%`S;6(th5A_-DKR3#`Rf{g>gwo8XbQ>F10+dnIm*eh$3gX7mfm!;V*r zUORaA;K&7Va4ip8CQ7Blb&}OKW$g-Cy98%YvF$fysDYBO?LQ z{3aHvalf?pB!rtnuUA^%FL9T`2kTI{{&tCK1=mwnP{XM0ove3PK3w_sY6wgPhf7-D zBXPYfNc$h|fBT^7;n&k1UPVjTcI^EvAMAK~6t>#OkVIK2KS&Sd}MVMDy#n z1kuN_lnSJI8!EB>RT2HuFl%|U^R1o-J#Vv8RMHEkYYFv^u~_LrkhzY1-)@60=Ljyuz7|i6@1w-bv1l zXrj6Nn`^}E%_BSLC>8U0`>X3-E4W)A6qdbT_Cd{iH9~8Ja}}5)nDY9Q zn6tGSXSK_;B<-AV(I%C33OXBe+mpas`YqN<^@GG>VGmeQg5YFW4`^tOP%jze-CTzUDMx z1)0lkF>8-d3P~Wz;jq)#cxT{~-29iVx2z*A5PdBd&YTyz+){3z8i3vLNlUjtSfz$= z!j392|2T@ls?`__nUJk7`CcBnH6#=izf<;Z^~377PpWFYR#j_4nVapS%f_xwa<$V$ z|A4si;Mk#wbfLaOID1~W$b)l}uyZrcyI5R#XsmsrKxlRfP2FH$gdWPikhyLXv$g}Y z0VN;PbwgOOU(7x*wpBRMDjaVU?A?=`OXcfzlp21q`kCrydB~k>Zqz(qJIaj(C9aYj zcfVBja?P!p+qGk<6R8qcM~-*DWO+IBR_5(_qm|>860YlDIDJri!V&P;(ur)Ue=p=r z#kY!Ymy8C-0lt!wt_pFhZdO3-6Sx}?3Xh2SM`0ImTgE%ze~(68a*{$ zF}_czZ<*xIsS_Zl4~p5vW2Yu+Cyof6U6Z)NfCz9M(LCz>TrXw4yz%jdzQ03{l)pOfDwsB&kgv}N{?gg$7-x|Jsb!_j%UJ0ke>ha;JwYx)WcZ+NHjyHjJ zNVupl#o;-&-aI+7Wn?u7O2xwpp|njbX_v5aspmBs92WDBNZe7tdWy>laXB|L?pwaj z-Q$F9mEz_qq5V7v`;%I-|JmXUAXRo4sdkFlyGBdKH;gY6&NS)5AY^Z(iE;eK@#m}W zcYfV@&nfJy7Pr^n(t}!ez*CCEN}6PiI_rf6Pk>Y1e|P^Y2gfQWDk$1$v;_YLUTb{6 zc!>B(T;-TX*gZPIg#~%9ZkbwF6k1nALV0Ojwba%z)n*H|*?=7OZUw1mJJ zo$D3PxB=2NrNVN`au)lDdSqb~%@%UXKFa!b_J`R*onNdDNL&z~!VEuu^R%$N0=V{3 z*|#e`tPsxn#acrTA$O;+dY1tC{O)QzXk+M1qtJ9sJoBW)J*5K*lN_s9a#6zNtZ>OG z6Z6X@t^!}FNY0(ZqlJ*VHNMv<)I2FxJ_VC$fM zK*@k7rvZ5^3tcaCzSwi4XT%~cL!+qT5I*ChV*W9SE7fP5dh-fYTe2Ty3;RxsyUqv> zC(QMPtpmku0afKWaSGC+Dc4;*pxLM*_2!T~AHo8PotW~R|ppLHVfy(@r zTU$naV~eGghbHz4?WAUf&Z=Atty$Q6gpIS#Ek2Gsd#GD*3hc0P=QK+nErN# zw5m*t7J(TZ$?Zs286n;bDsg3>Eu9N2 zSWZ+TCJR)Rc)nwF>G)F0utf3{v`6I>Pl#!Ulw|nE@bgzk_l|?aqmr6x7>%BV;+3#q z&8wEFyxpO^-D90nUa4@l8EWiG1c9za#&g}&y1k)wd&e!(x^ki6oM3lKijX62cE6PQ z^1@pSA&ETx;P@NW(()rpDU-9SqvH`?%e$R14SB@4*PwX3a2+gYdR3-tFL@11|lk%`{r6}+mk>o6C?Vj;Uq4Au=wW_#>7Hqj+ z_Vt>3HLur#ozD3%2eMay_gdsJ>Mesvp@K;tVRjP5jR)<$g_0Z`#%~VD4VbG~( zYE4mSO_8{UMn(}dD8~z{w}*C>3p*=5D*E=G5BG%5_{Eb3uW+;DW?g7onNV2%(YkMM z_;5q$v`4Jb)&l5oDO5rO)q5L&TE%-%ct)WlMVEzd!93s+t>s}@>uM9e-4 zMaPCtq24aIFHI^77gPsq_Pi6^_<+Ie3E4PR3R->)%1MxVr9#1fZW>H{LD<9(h}j2)L#Lri zE}U%g83B~-kntY;h^PaC@|oN3gH%Qs?%!gSn2g^JIq2J?2|sAC+IkA$4hVPm)> zv}%XAYS;Ku&`revE3>}UcC+^0p^ST5frjMPWMrv1!(oN=5Ea^wd1LvJ$i_T?Bmp$U7%8MN0l`tyc!lRuGM0G zje=yjlAjbe)(SQ|D0zj2teP8XsHkvgMTwYwICK;WMrgYLR4pfQO&^7AH)2JwgjyqI z(#o(fm5BUE{pclW`N4^$uw*N7RzgnIKaVVta<+{%O~B-}P%DOvhLa#XaJ`jzKkMt+ z_p(FVtHpvE!HTk)GNr_n3MD3kVu!Lf(P9)ki$knDe(U({YGZ;6bimRP+EVgEfRd^4d?SNh=$W2q(59B?mSLMYDkgSyMy&HJ z3bgUcEtp|S4It)7B*DYJ7QFGj^CPEWqsE5!HiVA5#4@!Q8_v;kF~0(eCdSY)l)Zsw zRjlfYLC?_QTie~XS2~d60B3_%*lAP8jOrs$AcUg8*S6f<@=D>DV**IipaXjp(uB4h z6$+2N@B1L|ULaKK7LWG<>xUp+M%6pKsvb_JQZc_w;>tf;k_m=!S=@$^iqMRT_grM1 z#D0pE?>|C%LlIJ`MT`%MM^D27wBYqiT!1h;ae)ivZWmVX2o+VpLZDJO2%UXWtVNmo zNqT!mI9*knY81a4#F|G8)gojZ&oM$ZEPJk(R!*(h7Fw}wG$5@g`KbQeEg!ZBu+!+& zd7%%j2BXa=NbQv!70BtThH<1pQbHg~l5s(XRNkmS0_!oOf~Z;Nb<%PaHNkRnsLCcD zF_z@PUh2Y)E0^J#5>7j{;%1~BaWtru_F#G`jhseN@&osH%XqaRN)E*xC5KfwFqNGz z3{U0chjQ|@tSl!yDEeMC;SmqZAcjxgWW*iji465bmU#;cX!hzgLE?sQ z7ryCxYw*F~L;o9B!?ROEZU=>|miJmhr@F;TmoS7j1k^yyA4!{1^GDJ~hEt0nV3E;o zB}~*vB2BD8xwLX;ct}HHE>@8whR4TaYW351*1enmF#o-cZ*LFhQD}i?=)uY4zn4F; z{u_nD1-sz!Nt_>$4`9tKxR=(!*qFWwEj)Op7qd{2wGgTr91cR(VU@ zt&k;_P+^Ife^}ywK8J!ZNDyguf9_C+S)hHr`5KvNJE<%Qt8mKKZ7sazS;S$>NQATei#avT! z;d`CZ>S@M|POj7#MkNv~0EM=f3EQ+no?om1zEVp>VUlYTOFAW7cMT^ZDJK}BW@5h3 z(r&2u6eQyl1K>C&l^v+aerc7r2o)Z}WTt470BotgDnl$cu5*EsA^T`V_1-BlzgXf9 zG1z@b+*kse5nvYt?3H7!@P<~PJ=ymzk8K!RHjz51EHuM3LfLx(9AZQyI~>C6j>UJ2 zN9(2a2PRUX;HhpkMKM7nQBdo-qbU-jRI3}zdNIEN9GkJ$7?y*xbdIhGs!=biy)h)MnpW>8syqdgN$?gF5JuWYY9U(;JPd>?tm09_>g_GC z5+gk$9dIWB_Z}Cjw28+e*Z|_V4Qo@n_o6*Acoh=5XfaynShBRD7*-wjf4E<0X#Qr2 zx_=zsVS$xv=3H=xSaM$CE@%>Tvl>=mD(+Q;c2$Ulm8dRiDBOp%Z3zYg0|G$uSL9P) zK7H%7wx+nc43<%0wU%1QilpZ1@ex)eN)t^r-dNruRKA}LTl?OlZ&ENUaDymTYLmZJ zbHD!UjrSTuMJL2<)le>H2ce2`)O^@B0Tobc{|ZqawBtRrtL(it?6W#UXFH+t#wXXK zRnYq629yV(rG40E+hplN#X0rE38gx;P-=M+bvc$&mV$N=r<_HFJN2VJ#hC(Kj-_oO8~gCEkm0`xAWh!{!c@@&&adUt5;bcMvFci&aWN zdoTdXF{m!1$>DplR1X&BjCrxdA@mPTs{7@PX70F{e?nOhQ7YJ>XL+_r4dqT&~ ziihe2rwfKb8~qTnXDn=<7mL}4LU4Oga~o{6gDrZK$~_vG*s=08;=4)g25Lo$(L{?z z7%6Q$B>^?V3|G=v>wem>YF%A*BC2g+R${QSsYX1!N`uc4A8nD=gnKN*at?SV=X=fx zr&!etTSMXIF^RjPX`t@Q)nbP+%DfSCeV;ZHwD{6y0_czKY@B93sN=eKF=hsA0X#33 zsA~`Ed3A@FzU*s=m5H6e1*z%Fwz`dt6huMoL{UahXkXQb^Ftj@X<0Ya5ZAXR$QbJ#}bs@C>_{4^ZWl%1g#F2!)BMYzJhv0t1nu#>D z&QI@dFt>zHxqNMHY1%FqTFEP@Rq&`gJwobEk1}DWmj3mNHR}DLY=w`xzOPd?BeZ@u zloU5S+z>k2CKk5~{iyifU?F#38Qo3nrGBtXdLW%j?b(fF`J!knshKji_i{d5BbJ_1 z@61u|^+e>*##6T=?R)#Edh0OWvJ_$7x(qEKVQI6ttwnH?8-P)~0yhFOwiSo)CURz_ zA$AGM5CT$q7p?592L1Qy;0F5-U7-suX?2e~Wy&WQEsz6*5!C+el#0z>uE5W65W!wwC2ia;M?7S#jo zyA73`g&liBL#tuy?F|x2#aE8jmVML!Vz0jJgYz=jQ#n#LYLjwsZAeW`qYFl?ElWm; z3X?bDg~<*RDiS97`0drvrANOdfmW6nQgtgVhdC#fs2e4X*5KZ0F~3CO4r|8XmR6KK zEN(m^G&I4zQ4m3Ywqic4A+1)|kYdKkx=AwLF<}T+DDOgQRgv!q(PCX4Q_XDEi`yE6 z?j8`qlNLy@Km=iXKI6ub5Z=K3V0uU#dv#pXbl5mUvmtC9!$GZksqq~v^)f79hwWm( zj4cSQDA2ZQt(dkCj*v0lGIYWM&Nfkz?pO&GlJ#?geY&CDUJHi;#Mjg}<;yH_jbAC$OaO_*+G+&K)j)WCy4sH8#M-zd0xL7Zx}`#i$(#8}%y z34`$+QFN$C~I!(CHbF%<_9Us$;tm_NlWJbV?k2#`tK(#`)*Rs z_mZ~!AT4R}+Nb9{yIxFM^YN~uPtSigc)j7~<~zG@Z5zoJmTn6z+%6^^{dm`2_L;pO wZ!dZ}^V!Pl>#twFlXBzQ9bU-V5}LPFOe*?enmZ-A@Ux5Qy_w1X-yq=!0FXQDm;e9( literal 0 HcmV?d00001 diff --git a/migrations/versions/fba92ed020b1_merge_identity_v1.py b/migrations/versions/fba92ed020b1_merge_identity_v1.py new file mode 100644 index 0000000..af8356f --- /dev/null +++ b/migrations/versions/fba92ed020b1_merge_identity_v1.py @@ -0,0 +1,945 @@ +"""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 ###