SPS V4 · Data provenance
The inbound companion to the pbi_v1 read contract. That page is how you read out of SPS; this one is how the data got in — the one-off Excel bootstrap, the Databricks gold sync that now overlays it, and the seam between them. Read this to trust the lineage and to see the exact transforms any replacement pipeline must reproduce.
SPS V4 was never fed by a live pipeline at launch. It was bootstrapped once from Excel, and financials are now kept current by a gold-layer sync. Three phases:
t0 · once
The whole DB, loaded from the historical Excel workbook + weekly submission files. Everything at launch came from here.
now · ongoing
Databricks sps_table2 overlays monthly Fakt financials + daily inventory onto
the Excel baseline. Money is source-of-truth here.
next · your call
Replace the manual Excel import with an automated source → SPS ingestion. Sections 04–06 are its spec.
| Phase | Cadence | Owns which data | Mechanism |
|---|---|---|---|
| Excel bootstrap | one-off (re-run on rebuilds) | Everything at t0 — projects, people, financials, weekly status, submissions, work volumes, materials, criticals | scripts/import-excel.ts |
| Gold sync | daily | Fakt financials (rev/costs/hours) + accruals + daily inventory snapshots — overlaid per month | scripts/sync-databricks-financials.ts |
| In-app entry | continuous | Everything Planas-side going forward — PVs submit weekly via the app | app submit pipeline (not covered here) |
The Excel import ran through dev and staging repeatedly during build, and a real production replace was executed on 2026-06-22 (~140 projects, identity preserved).
Two Excel inputs feed the bootstrap, plus one gold source for the ongoing sync.
DUOMENYS_PVT.xlsxOne workbook, eight sheets. The source of truth for projects, people, and the financial / status baseline.
SPS FAILAI 3/A folder of per-week, per-project macro workbooks. Filename encodes everything the walker needs; three form versions are supported.
2026-05-12_08_30 … forma V3.1 … _2114.xlsm
└ version └ project code
versions: V3 · V3.1 (primary/additional hours split) · V3.2 (adds daily inventory)
sheets read: SPSPF (D3 = project code) · IVESTIS (hours) · SDU (work volume m²) · KRITINES (critical activities)
sps_table2Per-Objektas monthly aggregate view in the Databricks Hive gold schema, read via the 1.2
Command Execution API on cluster adf, or from ADLS as an abfss:// export.
Feeds the ongoing financials/inventory sync (§06).
scripts/import-excel.ts orchestrates everything in two batches. Phase A reads the
central workbook; Phase B reads the submission folder; then three materialization passes project the
submissions into the normalized read tables.
prisma.$transaction (600 s timeout) — it either fully replaces the imported data
or rolls back. The Databricks sync and the FK-orphan sweep run after commit, as separate steps.| Step | Reads | Writes |
|---|---|---|
| A1 Projects + people | Projektai | projects, users, project_assignments |
| A2 Project extras | Projektu duomenys | projects (dates, type, stage, contract), AV assignment |
| A4 Financials | Pajamos_sanaudos, Papildomos_pajamos, Valandos_men | project_financials (per project/month) |
| A5 Weekly status | Projektu_stebesena, SPS_formai | project_status_weekly (fever + inventory cols) |
| B Submissions | SPS FAILAI 3/*.xlsm | weekly_submissions (immutable, versioned, snapshot JSONB) |
| Materialize status/volumes/latest | submitted weekly_submissions | work_volumes, critical_activities, project_material*, status/financials read models |
A3 (materials catalog rebuild) was removed — materials_catalog is now app-owned
and migration-seeded, so the import no longer touches it.
import_manifest; per-project wipe-and-rewrite for financials/status. Safe to re-run.weekly_submissions are immutable (ADR-021), versions restart at 1 per week; a second insert into an existing bucket violates the unique constraint and rolls the whole import back. Re-importing submissions means reset-then-rebuild, not incremental.| Command | Does |
|---|---|
| db:import-excel | The full bootstrap. Flags: --dry-run, --check-only, --replace-operational, --preserve-identity, --min-submissions N (floor gate). |
| db:replace-prod | Prod-only wrapper: preflight, JIT firewall, backup, atomic import with floor gate, gold sync, FK sweep, smoke tests. |
| db:sync-databricks-financials | Overlay sps_table2 gold financials + inventory (§06). --source <abfss|path>, --dry-run. |
| db:backfill-* | Idempotent live-DB fixups: contract-costs, hours, submissions-window, criticals-done, project logos. |
The centerpiece: what each Excel column becomes in Postgres, and what transform is applied on
the way. This is the spec a replacement pipeline must reproduce. Exact cell references live in the phase
parsers under scripts/import/excel/; the authoritative column-by-column list is
docs/spec-gates/field-mapping.csv.
Projektai → projects · users · project_assignments| Source | Target | Transform |
|---|---|---|
| code (A) | projects.id / code | Text PK — strings allowed (e.g. 2132_ADM). key |
| name (B) | projects.name | 1:1 |
| active (E) | projects.archived_at | active = 2 → archived_at = now. xform |
| country (F) | projects.country | ISO 3166-1 alpha-2 |
| PV name + email (G, H) | users + project_assignments kind=pv | Name → seed user (seed-{emailLocal} id), resolved to Entra OID at first SSO login. xform |
| PS name + email (I, J) | users + project_assignments kind=ps | Same as PV. |
Projektu duomenys → projects| Source | Target | Transform |
|---|---|---|
| start / sign / end dates | projects.start_date, contract_end_date, agreed_end_date | Renamed 1:1. |
| type | projects.type | Enum A / B / C / D. |
| stage | projects.stage | FK to reference.project_stages; Iniciavimas typo normalized to Inicijavimas. xform |
| contract revenue | projects.contract_revenue | EUR. |
| margin % (K) | projects.contract_costs | contract_costs = revenue × (1 − margin). Margin is a ratio < 1, not EUR; margin ≥ 1 → null (bad data). xform |
| contract hours | projects.contract_hours | 1:1. |
| AV name (O) | users + project_assignments kind=av | Name → Entra UPN via av-email-map.json (best-effort az ad user list); unresolved → placeholder {slug}@av-unresolved.sps.local. xform |
Pajamos_sanaudos · Papildomos_pajamos · Valandos_men → project_financialsAll three aggregate to one row per (project, month-end). key: project_id, month
| Source | Target | Transform |
|---|---|---|
| Pajamos_sanaudos · revenue | rev_primary | Aggregated per project/month; cost-group dimension collapsed (all → primary). |
| Pajamos_sanaudos · costs | costs_primary | Same aggregation. |
| Papildomos_pajamos · additional | rev_additional | Aggregated, then upserted onto the primary rows. xform |
| Valandos_men · hours | hours_primary | Aggregated per project/month. |
| Valandos_men · actual / planned workers | actual_workers, planned_workers | 1:1. |
| month date | month | Normalized to month-end ISO date. xform |
project_financials.
Submissions and the gold sync overlay months onto this baseline (upsert by key) — they never wipe a
project. A sparse snapshot can't erase the central-workbook series.Projektu_stebesena · SPS_formai → project_status_weeklyWeekly rows keyed by ISO week. key: project_id, year, week_number
| Source | Target | Transform |
|---|---|---|
| Projektu_stebesena · week date | year, week_number | Converted to ISO year + week. xform |
| buffer % / work % / expected end / cum. hours | buffer_consumed_pct, work_done_pct, expected_end_date, hours_done_cum | Fever-chart columns. Bypass submissions — Phase A owns them. |
| SPS_formai · sukaupimai (Q) | sukaupimai | Accruals, per week. |
| SPS_formai · atsargos viso (W) / >90d (X) | atsargos_viso, atsargos_90d | 1:1. |
| — derived — | atsargos_30d | atsargos_30d = max(0, viso − 90d). xform |
.xlsm → weekly_submissions.snapshot_data (v1 JSONB) → read modelsEach submission parses into an immutable v1 JSON snapshot, then materializes into the
normalized tables. Shape contract: docs/spec-gates/snapshot-v1-schema.md.
| Source (sheet) | Materialized target | Transform |
|---|---|---|
| SDU grid (m² by week) | work_volumes (planned, actual) | Union of all submissions, highest version per week, replayed oldest-first. xform |
| KRITINES | critical_activities | Latest snapshot; done flag → ISO date (§05); owner resolved or null. |
| materials block | project_material · project_material_months | Resolved to catalog FK; duplicate (type,name) merged, quantities summed; positional monthly array → keyed by month-end. xform |
| IVESTIS hours / status | project_financials · project_status_weekly | Overlaid onto Phase A baseline (upsert), never wipe. |
The non-obvious conversions. Any pipeline that replaces the Excel import has to get these right or the numbers drift.
contract_costs = revenue × (1 − margin).
Feed the raw margin as if it were EUR and every contract cost is wrong.sps_table1)
summed additional costs into the primary figure. sps_table2 fixed it —
costs_primary = Sanaudos − Papildomos. If you re-derive financials from an older source,
check which convention it follows.month is month-end. Every financial row is keyed to
the last day of the month, not the first. Match that grain or joins miss.+ / √
for done; the schema wants an ISO completion date. Conversion takes the earliest submission where
the task was marked done; - / X / blank → null. Not a boolean.work_volumes has no Phase-A baseline. Valandos_sav
is weekly hours, not m². Work volume comes only from submission SDU sheets.(type, name) rows; they're merged by catalog FK with quantities summed, else you double-count.The Excel import set the financial baseline. From there, the Databricks
sps_table2 sync keeps financials current — this is the boundary between "how it started" and
"how it runs now."
(project, month)
onto the Phase-A baseline. It touches rev/costs/hours + accruals and writes daily
project_inventory_snapshot rows for atsargos. It never wipes a project.financial_close_date is the Fakt/Planas boundary. Months on or
before it are Fakt — import/sync-owned, read-only in the app. Months after it are
Planas — PV-editable. The sync advances the close date as new actuals land. Full semantics:
docs/design/2026-05-21-marza-faktine-semantics.md._ADM codes are dropped; only known project codes are synced.(project, month) /
(project, date).Stated plainly so you can reconcile against them rather than discover them.
hours_primary /
hours_additional are not direct/indirect until Diana's A9 contract signs.
Don't repurpose them into that meaning.(type, name) isn't in materials_catalog is logged to
scripts/out/sps-import-skipped.json and dropped — the import still commits.az ad user list; unresolved AVs get a placeholder domain. Treat AV identity as low-confidence
until reviewed.weekly_submissions are written status='submitted' but don't necessarily satisfy
the live-submit invariants (work-volume presence, 5/5 touch coverage). Read models stay populated because
they source from Phase A + the union, not from any single snapshot's completeness.The manual Excel import was a bootstrap, not a pipeline. The sustainable version is an automated source → SPS ingestion (Rivile / gold → the same Postgres tables). Sections 04–06 are its functional spec — reproduce those transforms and the boundary rules and the read models stay correct.
Open items to scope if that's the direction:
(type, name) beyond "skip + log".Everything named on this page, in the SPS V4 repo.
| What | Path |
|---|---|
| Import runbook (phases, idempotency, reset) | docs/runbooks/excel-data-import.md |
| Authoritative field map (65 rows) | docs/spec-gates/field-mapping.csv |
| Snapshot v1 JSONB contract | docs/spec-gates/snapshot-v1-schema.md |
| Fakt / Planas semantics | docs/design/2026-05-21-marza-faktine-semantics.md |
| Import orchestrator + phase parsers | scripts/import-excel.ts · scripts/import/excel/ |
| Materialization | scripts/import/materialize.ts |
| Gold sync (parser + planner + reader) | scripts/sync-databricks-financials.ts · src/lib/databricks/ |
| Outbound read contract (the other brief) | docs/datalake-access-page/ · sps-datalake-contract.pages.dev |
sps_table2 gold sync, and everything else is now entered in-app. Anything you want
to reconcile, re-derive, or automate beyond that is a short conversation with Vilius.