harden(epay): cart-hygiene invariant uses confirmed cart count + add service architecture plan

- cartCount tracks actual cart rows (decrement only on confirmed delete) so a
  failed cleanup delete can't trigger a false dirty-cart abort.
- docs/plans/006: the multi-tenant CF-service architecture (DB-backed
  fulfiller, account pool, catalog dedup, per-tenant credential model,
  reversible flag flip) — the executable next phase. The Phase-F flag flip is
  gated on the orchestrator fulfiller existing (Plan 003 Faza F was wrong).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-06-05 00:06:06 +03:00
parent f49fdb1da0
commit 28c870fb12
6 changed files with 1703 additions and 11 deletions
+285
View File
@@ -0,0 +1,285 @@
# Plan 001 — DB GIS Central Multi-App
**Status:** DRAFT v1 — neaprobat, de discutat
**Autor:** Claude + Marius Tarau
**Data:** 2026-04-18
**Scope:** Arhitectură unificată GIS pentru ArchiTools, eTerra.live, SmartCity360, Planhub ERP + servicii viitoare
---
## 1. Context & motivație
Date GIS (cadastru ANCPI, enrichment, PUG/PUZ, UTR) sunt folosite de multiple platforme:
- **ArchiTools** — portal intern Beletage (există, producție)
- **eTerra.live** — SaaS topografi (proxy eTerra, repo nou gitadmin/eterra-live)
- **SmartCity360** — portal GIS primării (în arhitectură)
- **Planhub ERP** — ERP arhitecți cu geoportal (în arhitectură)
- Viitoare servicii
Nevoie: **single source of truth GIS** cu acces controlat, sync centralizat, cache/CDN partajat, izolare multi-tenant.
---
## 2. Topologie
**All on-prem.** Nimic în cloud public.
- Servere: satra (10.10.10.166), shop (10.10.10.84), proxy (10.10.10.199), orchi (10.10.40.60 — CJ2 via VPN)
- Gateway public: Traefik pe proxy, Sophos DNAT 80/443
- Cloudflare doar pentru domenii SaaS public-facing (eTerra.live, SmartCity360)
---
## 3. DB Central — Shop server (10.10.10.84)
**Resurse disponibile (verificat 2026-04-18):**
- Xeon Gold 6430, 128 vCPU, 251GB RAM, 3.5TB NVMe (1% ocupat)
- Rulează deja: Supabase stack, WordPress, Tegola
- Load avg ~0.5, headroom masiv
**Cluster Postgres nou, dedicat:**
- Container `postgres-gis` (separat de Supabase existent — izolare critică)
- Postgres 16 + PostGIS 3.4 + pg_cron
- SRID nativ 3844 (Stereo70)
- Port intern, expus doar pe 10.10.10.0/24 + VPN CJ2 (10.10.40.0/24)
- NICIODATĂ expus direct pe internet
### Schema split
| Schema | Conținut | Access |
|---|---|---|
| `gis_core` | Cadastru național ANCPI: UAT, terenuri, clădiri, administrativ | Public read, write doar sync daemon |
| `gis_urban` | PUG, PUZ, PUD, UTR, zone protecție, zone reglementate | Read public (layer public), write per tenant primărie (RLS) |
| `gis_enrichment` | CF, proprietari, adrese, categorii folosință — **GDPR sensibil** | Auth obligatoriu, RLS pe rol |
| `gis_meta` | Sync runs, rules, status, audit | Internal only |
| `tenants` | Primării, firme arh, clienți SaaS | Control plane |
### Roluri Postgres
- `gis_sync_rw` — write pe `gis_core` + `gis_meta` (sync daemons)
- `gis_public_ro` — read pe `gis_core` + layer public din `gis_urban` (Martin public tiles)
- `gis_app_ro` — read authenticated cu RLS pe `gis_urban` privat + `gis_enrichment`
- `gis_urban_rw` — write pe `gis_urban` doar pentru tenant-ul propriu
### RLS pe multi-tenant
```sql
-- exemplu pentru gis_urban tabele private
CREATE POLICY tenant_isolation ON gis_urban.puz_private
USING (siruta = ANY(current_setting('app.allowed_sirutas', true)::text[]));
```
---
## 4. Layer acces — public/auth endpoints
| Subdomain | Serviciu | Auth | Scop |
|---|---|---|---|
| `tiles.beletage.ro` | Martin (instanță dedicată) | Niciun auth pe layer public | Tile server public cadastru + PUG public |
| `pmtiles.beletage.ro` | nginx static | Niciun auth | Overview Romania, PMTiles pre-generated |
| `gis-api.beletage.ro` | Next.js API sau PostgREST | JWT Authentik | Enrichment, query spațial, mutations |
| `sync.beletage.ro` | Orchestrator UI | Authentik admin | Control panel sync central |
| `eterra.live` | SaaS topografi | Authentik | Portal topografi |
| `planhub.beletage.ro` | ERP arhitecți | Authentik | Geoportal + PM |
| `<primarie>.smartcity360.ro` | Portal primărie | Cetățeni anonim + admin Authentik | Subdomain per tenant |
**Traefik** — reverse proxy unic, SSL auto. **Cloudflare** în față pe domenii SaaS public (eTerra.live, SmartCity360) cu rate limit + CDN cache tile-uri (TTL 24h pentru UAT).
---
## 5. ACL Enrichment (per rol)
| Rol | Vede enrichment |
|---|---|
| `topograf` (eTerra.live) | Full — CF, proprietari, adrese |
| `architect` (Planhub ERP) | Full + PUG complet |
| `admin_primaria` (SmartCity360) | Full pentru UAT-ul primăriei + date PUG private |
| `cetatean` (SmartCity360 public) | Doar nr. cadastral + suprafață + UTR |
| `admin` (ArchiTools) | Tot |
Implementat prin RLS + `current_setting('app.role')`.
---
## 6. Shared enrichment pool — insight cheie
**De-duplicare globală.** Dacă topograf A cere enrichment pe parcela X:
1. Salvat în `gis_enrichment` central
2. Topograf B cere aceeași parcelă → **hit instant**, zero request la eTerra
3. TTL default 90 zile → după expire auto-refresh
Beneficii:
- Economii masive de sesiuni eTerra
- Viteză maximă pentru request-uri repetate
- Baza de date crește cu datele "cele mai cerute" de topografi (natural prioritization)
**TBD:** TTL fix 90 zile vs decizie manuală a topografului pentru "force refresh"?
---
## 7. Sync workers — centralizat
**Extrage din ArchiTools într-un serviciu standalone** `gis-sync-orchestrator`:
- Un singur scheduler (nu duplicat per app)
- Workers:
- `eterra-sync` — delta sync cadastru (nocturn automat + on-demand per UAT)
- `enrichment-worker` — pulls CF/owners pe cerere
- `pug-ingest` (viitor) — import PUG din shapefiles/PDFs
- **Queue:** `pg-boss` (Postgres-backed, zero infra extra) sau BullMQ+Redis
- **Delta sync** folosește `gis_uats.last_updated_dtm` deja existent
- **Notificare apps** la sync complete: `Postgres LISTEN/NOTIFY` + webhook → fiecare app se abonează
- **PMTiles rebuild** trigger-uit de orchestrator după sync major (webhook existent satra:9876)
### Session pool eTerra per-user
- Tabel `eterra_sessions` keyed by `user_id` (fiecare topograf = sesiune proprie)
- Sync on-demand eTerra.live folosește credențialele topografului logat
- Sync nocturn automat folosește cont service (actualul hardcoded)
- Avantaj: toți topografii + sistemul automat contribuie la menținerea DB-ului actual
---
## 8. Consumers
| App | Schemas | Write | DB user | Deploy |
|---|---|---|---|---|
| ArchiTools | core + urban + enrichment | enrichment | `archi_rw` | satra |
| eTerra.live | core + enrichment (ACL topograf) | enrichment via sync | `eterra_ro` + `eterra_sync` | satra/shop TBD |
| SmartCity360 | core + urban (filter siruta) | urban (PUG/PUZ) | `sc_rw` | TBD |
| Planhub ERP | core + urban | proiecte proprii (schemă `erp`) | `erp_rw` | TBD |
Fiecare app:
- Own Authentik OIDC app
- Own DB user + permisiuni minimale
- Own MinIO bucket
- Own Infisical project pentru secrete
---
## 9. Securitate
- **DB niciodată expus direct pe internet** → tot prin Martin / API cu auth
- **Enrichment** (CF, proprietari) → NU în tile-uri publice, doar via API auth'd
- **Rate limit Cloudflare** pe `tiles.*` (ex: 100 req/s per IP)
- **Audit log** pe `gis_urban` în `gis_meta.audit` — cine modifică PUG primăriei
- **Secrete** în Infisical, proiecte separate per app (zero reuse)
- **Backup:** `pg_basebackup` + WAL archiving zilnic → MinIO satra + replică async CJ2 (PITR 14 zile)
---
## 10. Performanță
- Tabele simplificate pe zoom (deja există pentru UAT: z0/5/8/12) — extinde pe `gis_terenuri` dacă nevoie
- **Materialized views** pentru query-uri grele cross-tenant (refresh nocturn)
- **Indexe GIST** pe toate `geom`, **BRIN** pe `created_at` pentru audit
- Martin pool: 16 conexiuni per instanță, 2 instanțe behind Traefik LB
- **Read replica Postgres** când QPS > 500/s (streaming replication)
- **PMTiles** pentru tot ce nu se schimbă zilnic (UAT, administrativ) — mult mai rapid decât Martin live
---
## 11. Scale target an 1
- ~50 arhitecți (Planhub ERP)
- ~100 topografi (eTerra.live)
- ~2 primării × 5 admin = 10 admin_primaria (SmartCity360)
- Citizen public access (SmartCity360) — nedeterminat
**Concluzie:** Shop server (128 vCPU, 251GB RAM) = zero probleme la scale-ul ăsta. Rezerve pentru 10x creștere.
---
## 12. Migrare graduală — fără downtime ArchiTools
1. Setup `postgres-gis` container pe shop, config de bază
2. Replicare initial snapshot `architools_db``postgres-gis` (pg_dump + restore)
3. Creează schemas noi (`gis_urban`, `tenants`), mută tabele existente (`ALTER TABLE SET SCHEMA`)
4. Creează roluri + RLS policies, testează cu user nou
5. Extrage sync în serviciu standalone — paritate funcțională cu ArchiTools
6. Lansează Martin dedicat pe `tiles.beletage.ro`
7. ArchiTools trece pe endpoint nou (feature flag)
8. Verify paritate + performanță → cutover final
9. eTerra.live + SmartCity360 + Planhub ERP conectate la DB central
---
## 13. Date GIS existente (autoritative)
| Tabel | Conținut | Features aprox |
|---|---|---|
| `gis_uats` | Limite UAT Romania (+ views z0/5/8/12 simplified) | 3181 UAT-uri |
| `gis_administrativ` | Intravilan + arii speciale ANCPI | — |
| `gis_terenuri` | Parcele cadastrale eTerra | Cluj 774K (extrapolare ~25M total Romania) |
| `gis_cladiri` | Clădiri cadastrale eTerra | Cluj — (similar magnitude) |
| `gis_terenuri_status` / `gis_cladiri_status` | Parcele/clădiri + flag enrichment/legal | — |
| `gis_features` | Raw eTerra (attributes JSON + enrichment JSON) | — |
| `gis_sync_rules`, `gis_sync_runs` | Orchestrare sync | — |
Status actual: Cluj complet sincronizat (81 UAT, ~8.3h), restul României de populat (estimare 15-25h inițial, 1-2h delta).
---
## 14. Date GIS de adăugat pentru SmartCity360 + ERP
**Urbanism PUG:**
- `gis_urban.utr` — Unități Teritoriale Referință (code, zone_type, regulation JSON cu POT/CUT/H_max, geom)
- `gis_urban.zone_reglementate` — zone funcționale PUG
- `gis_urban.zone_protectie` — monumente, arheologie, peisaj, sanitară, hidrologică
**PUZ/PUD:**
- `gis_urban.puz` / `gis_urban.pud` — doc_ref, HCL_number, approval_date, status, geom perimetru
**Administrativ suplimentar:**
- `gis_urban.strazi`
- `gis_urban.numerotare`
- `gis_urban.dotari_publice` (școli, spitale, parcuri)
**Fază 2 (opțional):**
- Rețele utilități (apă, canal, gaz, electric, telecom)
- Cadastru verde (arbori, scuaruri)
---
## 15. Întrebări deschise — TBD
### Tehnice
1. **Supabase vs Postgres raw pe shop** — schemă nouă în Supabase existent (câștig: RLS auto, PostgREST gratis, auth) sau container Postgres separat (izolare totală, simplu)? **Recomandare curentă:** separat.
2. **Domain SaaS**`eterra.live` deja deținut? `smartcity360.ro` de cumpărat? Subdomain per primărie (`cluj-napoca.smartcity360.ro`) vs path-based (`smartcity360.ro/cluj-napoca`)?
3. **Portal cetățeni SmartCity** — acces complet anonim (zero login, doar rate limit) vs email/captcha pentru abuse prevention?
4. **TTL enrichment** — 90 zile auto-refresh implicit? Topograf poate forța refresh manual?
5. **Backup offsite** — replică Postgres async la CJ2 (10.10.40.x) via VPN? Sau doar MinIO snapshot zilnic?
6. **Migrare ArchiTools** — DB-ul actual rămâne paralel cu noul central, apoi cutover? Sau migrare directă cu downtime controlat?
7. **Martin shared vs per-app** — o singură instanță Martin publică pentru toate app-urile, sau instanțe separate per app (branding URL + izolare)?
### Business / organizaționale
8. **Billing / metering** — eTerra.live SaaS plătit: how measured? Per sync on-demand? Per enrichment request? Per user/lună?
9. **Data ownership** — dacă topograf X face enrichment pe parcela Y, datele sunt ale lui privat sau contribuie automat la pool-ul central public? (răspuns parțial: shared pool, dar GDPR?)
10. **PUG private pentru primării** — ce date specific sunt "private" vs "public"? (ex: observații interne, documentație anexă)
11. **SLA public tiles** — uptime target? Recovery time după outage?
---
## 16. Concluzii curente
- Arhitectura e fezabilă pe infrastructura existentă fără hardware nou
- Shop server = candidat ideal DB central
- Shared enrichment pool = insight major cu ROI imediat
- Migrarea se poate face gradual fără downtime
- Securitate + multi-tenancy rezolvate cu RLS + Authentik per-app
**Next steps când e aprobat:**
1. Răspuns la întrebările deschise (secțiunea 15)
2. Design detaliat schema `gis_urban` (DDL concret)
3. Design API `gis-api.beletage.ro` (endpoints + auth flow)
4. Plan migrare ArchiTools pas-cu-pas
5. POC postgres-gis pe shop + test Martin + 1 app consumer