Files
ArchiTools/prisma/schema.prisma
T
Claude VM 71cfc29f9a feat(geoportal-v2): export toolbar + Semnez ca picker + CF intern/Extras split
V2 panel toolbar replaces the single "Comandă CF" button with two rows:

  [Încadrare] [Pl. situație] [Coord.] [DXF]       ← 4 exports
  [CF intern] [Extras CF]                          ← 2 CF flows

Each export button pops an inline modal:
  - PIZ / PAD: SignAsPicker (PFA / PJA radio list, manual-add inline,
    co-signer slot on PIZ) + basemap toggle (google / orto for PIZ).
  - Coord / DXF: no picker — single-click download via JWT proxy.

"CF intern" is the free copycf flow from eTerra (proxied via gis-api);
"Extras CF" keeps the existing CfOrderModal (1 credit ePay). The two
modes are now visually balanced as a 2-button row.

Sign-as picker rows merge user-owned Signatory table entries with the
SIGN_AS_DEFAULT_OPTIONS env-driven fallback (org-wide hardcoded options;
defaults seed two Studii de teren entries — Tiurbe PFA + SRL PJA). New
rows added via the picker's "Adaugă autorizație" inline form write to
the Signatory table; ENV rows are read-only.

Architots side ships fully:
  - prisma Signatory model + ALTER TABLE applied (per the schema-drift
    feedback memory).
  - /api/sign-as-options (GET, POST) + /api/sign-as-options/[id]
    (PATCH, DELETE).
  - /api/cf-intern/order and /api/gis/parcel/[id]/{piz,pad,coords,dxf}
    proxy routes — auth check + JWT forward, stream binary back.
  - gis-api thin client extended with the matching exports.* namespace.

Until the gis-api endpoints ship (next session — full spec in
docs/plans/005-gis-api-export-endpoints.md), each export proxy returns
501 "…urmează" with a Romanian message so the modal shows what's
coming instead of a hard error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:57:55 +03:00

227 lines
8.3 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model KeyValueStore {
id String @id @default(uuid())
namespace String
key String
value Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([namespace, key])
@@index([namespace])
}
// ─── GIS: Sync Scheduling ──────────────────────────────────────────
model GisSyncRule {
id String @id @default(uuid())
siruta String? /// Set = UAT-specific rule
county String? /// Set = county-wide default rule
frequency String /// "3x-daily"|"daily"|"weekly"|"monthly"|"manual"
syncTerenuri Boolean @default(true)
syncCladiri Boolean @default(true)
syncNoGeom Boolean @default(false)
syncEnrich Boolean @default(false)
priority Int @default(5) /// 1=highest, 10=lowest
enabled Boolean @default(true)
allowedHoursStart Int? /// null = no restriction, e.g. 1 for 01:00
allowedHoursEnd Int? /// e.g. 5 for 05:00
allowedDays String? /// e.g. "1,2,3,4,5" for weekdays, null = all days
lastSyncAt DateTime?
lastSyncStatus String? /// "done"|"error"
lastSyncError String?
nextDueAt DateTime?
label String? /// Human-readable note
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([siruta, county])
@@index([enabled, nextDueAt])
@@index([county])
@@index([frequency])
}
// ─── GIS: eTerra ParcelSync ────────────────────────────────────────
model GisFeature {
id String @id @default(uuid())
layerId String // e.g. TERENURI_ACTIVE, CLADIRI_ACTIVE
siruta String
objectId Int // eTerra OBJECTID (unique per layer); negative for no-geometry parcels (= -immovablePk)
inspireId String?
cadastralRef String? // NATIONAL_CADASTRAL_REFERENCE
areaValue Float?
isActive Boolean @default(true)
attributes Json // all raw eTerra attributes
geometry Json? // GeoJSON geometry (Polygon/MultiPolygon)
geometrySource String? // null = normal GIS sync, "NO_GEOMETRY" = eTerra immovable without GIS geometry
// NOTE: native PostGIS column 'geom' is managed via SQL trigger (see prisma/postgis-setup.sql)
// Prisma doesn't need to know about it — trigger auto-populates from geometry JSON
enrichment Json? // magic data: CF, owners, address, categories, etc.
enrichedAt DateTime? // when enrichment was last fetched
syncRunId String?
projectId String? // link to project tag
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
syncRun GisSyncRun? @relation(fields: [syncRunId], references: [id], onDelete: SetNull)
@@unique([layerId, objectId])
@@index([siruta])
@@index([cadastralRef])
@@index([layerId, siruta])
@@index([projectId])
@@index([geometrySource])
}
model GisSyncRun {
id String @id @default(uuid())
siruta String
uatName String?
layerId String
status String @default("pending") // pending | running | done | error
totalRemote Int @default(0)
totalLocal Int @default(0)
newFeatures Int @default(0)
removedFeatures Int @default(0)
startedAt DateTime @default(now())
completedAt DateTime?
errorMessage String?
features GisFeature[]
@@index([siruta])
@@index([layerId])
@@index([siruta, layerId])
}
model GisUat {
siruta String @id
name String
county String?
workspacePk Int?
geometry Json? /// EsriGeometry { rings: number[][][] } in EPSG:3844
areaValue Float? /// Area in sqm from LIMITE_UAT AREA_VALUE field
lastUpdatedDtm String? /// LAST_UPDATED_DTM from eTerra — for incremental sync
updatedAt DateTime @updatedAt
@@index([name])
@@index([county])
}
// ─── Registratura: Atomic Sequences + Audit ────────────────────────
model RegistrySequence {
id String @id @default(uuid())
company String // B, U, S, G (single-letter prefix)
year Int
type String // SEQ (shared across directions)
lastSeq Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([company, year, type])
@@index([company, year])
}
model RegistryAudit {
id String @id @default(uuid())
entryId String
entryNumber String
action String // created, updated, reserved_created, reserved_claimed, late_registration, closed, deleted
actor String
actorName String?
company String
detail Json?
createdAt DateTime @default(now())
@@index([entryId])
@@index([company, createdAt])
}
// ─── ANCPI ePay: CF Extract Orders ──────────────────────────────────
model CfExtract {
id String @id @default(uuid())
orderId String? // ePay orderId (shared across batch items)
basketRowId Int? // ePay cart item ID
nrCadastral String // cadastral number
nrCF String? // CF number if different
siruta String? // UAT SIRUTA code
judetIndex Int // ePay county index (0-41)
judetName String // county display name
uatId Int // ePay UAT numeric ID
uatName String // UAT display name
prodId Int @default(14200)
solicitantId String @default("14452")
status String @default("pending") // pending|queued|cart|searching|ordering|polling|downloading|completed|failed|cancelled
epayStatus String? // raw ePay status
idDocument Int? // ePay document ID
documentName String? // ePay filename
documentDate DateTime? // when ANCPI generated
minioPath String? // MinIO object key
minioIndex Int? // file version index
creditsUsed Int @default(1)
immovableId String? // eTerra immovable ID
immovableType String? // T/C/A
measuredArea String?
legalArea String?
address String?
gisFeatureId String? // link to GisFeature
version Int @default(1) // increments on re-order
expiresAt DateTime? // 30 days after documentDate
supersededById String? // newer version id
requestedBy String?
errorMessage String?
pollAttempts Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
// DB columns historically added by hand (not via Prisma migrate).
// Surfaced in the schema so callers can set/read them without raw
// SQL. See feedback_cfextract_schema_drift.md (2026-05-20).
userId String? // Authentik sub of the orderer
type String? @default("epay") // 'epay' | 'admin'
pdfData Bytes? // legacy inline PDF storage (most rows use minioPath instead)
adminOrderedBy String? // admin user who placed the order on someone's behalf
@@index([nrCadastral])
@@index([status])
@@index([orderId])
@@index([gisFeatureId])
@@index([createdAt])
@@index([nrCadastral, version])
@@index([userId])
@@index([userId, nrCadastral])
}
// ─── Geoportal Exports: Signatories (PIZ/PAD "Semnez ca:") ─────────
// Each row is an authorisation a user can sign exports with: themselves
// as a PFA (kind=user) or on behalf of an org/PJA (kind=org). The picker
// merges these rows with the SIGN_AS_DEFAULT_OPTIONS env-driven fallback
// (hardcoded org-wide entries) so a fresh install has usable defaults
// before anyone configures their own.
model Signatory {
id String @id @default(uuid())
userId String /// Authentik sub of the owner
kind String /// 'user' | 'org'
displayName String /// "Dan-Gheorghe Tiurbe" or "Studii de teren SRL"
authClass String? /// "Cat. D" / "Clasa III"
authNumber String /// "RO-B-F/3183"
isDefault Boolean @default(false) /// primary signer for this user
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([userId, isDefault])
}