Files
0pt/docs/03-data-model.md
T
adminandClaude Opus 4.8 22e09d984d feat: rebrand MONO->0pt (hero, header, layout) + add project docs/contract
Hero is now a data-native 0pt artifact (no photos, keeps grain/reveal/float); header is the 0pt wordmark with Swedish nav; layout is sv + loads JetBrains Mono + 0pt metadata; globals wires the loaded fonts. Adds CLAUDE.md + docs/01-09 as the build contract. next build green (5 routes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:38:07 +02:00

202 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 · Data model
Postgres is the backbone. Two halves: **data entities** (what we sell) and **platform tables** (orders, users, jobs). Every fact links to `bridge_provenance`. Migrations via Alembic (Python) or `sql/` migrations checked into the repo — pick one and be consistent; align with existing `src/workers/db.py`.
## Conventions
- `snake_case`, UTC timestamptz, soft-delete via `deleted_at` where useful.
- Natural keys for registries (`org_nr`), surrogate `uuid` PKs for platform tables.
- `created_at` / `updated_at` on every table.
- All monetary amounts in **öre** (integer) to avoid float errors; currency `SEK` default.
## Data entities
```sql
-- A company (the core registry entity)
CREATE TABLE dim_company (
org_nr text PRIMARY KEY, -- national reg. number (per market format)
market text NOT NULL, -- ISO country code: SE/NO/DK/FI/...
lei text, -- GLEIF cross-border key
name text NOT NULL,
status text, -- active/dissolved/...
nace text, -- NACE/SNI code
registered_at date,
address jsonb,
website text,
updated_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE fact_financials (
org_nr text REFERENCES dim_company(org_nr),
fiscal_year int NOT NULL,
revenue bigint, -- öre
result bigint,
employees int,
source_id text REFERENCES dim_source(source_id),
PRIMARY KEY (org_nr, fiscal_year)
);
CREATE TABLE dim_contact (
contact_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_nr text REFERENCES dim_company(org_nr),
name text,
role text,
email text,
phone text,
linkedin text,
-- PII: subject to GDPR; lawful basis recorded per delivery
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE dim_ubo ( -- ultimate beneficial owner
ubo_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_nr text REFERENCES dim_company(org_nr),
owner text,
share numeric(5,2)
);
CREATE TABLE dim_vehicle ( -- VehIQ synergy
vin text PRIMARY KEY,
market text NOT NULL,
reg_plate text,
make text, model text, year int,
owner_org text REFERENCES dim_company(org_nr),
specs jsonb,
updated_at timestamptz NOT NULL DEFAULT now()
);
```
## Provenance model (the signature — every fact links here)
```sql
CREATE TABLE dim_source (
source_id text PRIMARY KEY, -- 'gleif','brreg','companies_house',...
name text NOT NULL,
market text, -- or 'global'
access text, -- 'api'|'bulk'|'authorized_web'|'licensed'
licence text, -- short licence note
cadence text, -- 'daily'|'weekly'|'annual'|...
url text
);
-- One row per (entity field) we deliver, linking value → where it came from + confidence
CREATE TABLE bridge_provenance (
field_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type text NOT NULL, -- 'company'|'contact'|'vehicle'|'financials'
entity_key text NOT NULL, -- org_nr / vin / contact_id
field_name text NOT NULL, -- 'revenue','email',...
source_id text REFERENCES dim_source(source_id),
freshness date, -- when this value was confirmed
confidence numeric(4,3) NOT NULL, -- 0.000–1.000
level smallint NOT NULL, -- 0 unavailable/licensed, 1 single, 2 partial, 3 corroborated
agreed_sources int DEFAULT 1, -- # independent sources that agree
UNIQUE (entity_type, entity_key, field_name)
);
CREATE INDEX ON bridge_provenance (entity_type, entity_key);
```
> Rule: **no value ships without a `bridge_provenance` row.** Level 0 = delivered as "not available / licensed", never a guessed value.
## Platform tables
```sql
CREATE TABLE orgs (
org_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL, created_at timestamptz DEFAULT now()
);
CREATE TABLE users (
user_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
email text UNIQUE NOT NULL,
role text NOT NULL DEFAULT 'member', -- 'owner'|'admin'|'member'|'siax_admin'
created_at timestamptz DEFAULT now()
);
CREATE TABLE orders (
order_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
flow text NOT NULL, -- 'catalog'|'custom'|'subscription'
status text NOT NULL DEFAULT 'draft', -- see lifecycle in docs/01
config jsonb NOT NULL, -- configurator state (markets, fields, ...)
total_ore bigint, deposit_ore bigint, balance_ore bigint,
est_days int, est_confidence numeric(4,3),
created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now()
);
CREATE TABLE order_items ( -- resolved line items / dataset refs
item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
kind text, ref text, qty int, meta jsonb
);
CREATE TABLE jobs ( -- one production run per order
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
stage text, -- sourcing|extraction|structuring|enrichment|qa
pct numeric(5,2) DEFAULT 0,
state text DEFAULT 'queued', -- queued|running|done|failed
metrics jsonb, -- rows, confidence, per-market progress
started_at timestamptz, finished_at timestamptz
);
CREATE TABLE deliveries (
delivery_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
formats text[], destinations text[],
record_count int, avg_confidence numeric(4,3),
quality_report jsonb, provenance_export_url text,
files jsonb, -- [{format,url,bytes}]
created_at timestamptz DEFAULT now()
);
CREATE TABLE subscriptions (
sub_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
cadence text, -- weekly|daily
webhook_url text, status text DEFAULT 'active',
next_run_at timestamptz
);
CREATE TABLE files ( -- customer uploads
file_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
order_id uuid REFERENCES orders(order_id),
name text, mime text, bytes bigint, r2_key text,
created_at timestamptz DEFAULT now()
);
CREATE TABLE db_grants ( -- customer read-only DB access (encrypted)
grant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
order_id uuid REFERENCES orders(order_id),
conn_encrypted bytea NOT NULL, -- encrypted connection string
scope text, expires_at timestamptz, revoked_at timestamptz,
created_at timestamptz DEFAULT now()
);
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
actor text, action text, target text, meta jsonb,
at timestamptz DEFAULT now()
);
CREATE TABLE payments (
payment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
kind text, -- 'deposit'|'balance'
amount_ore bigint, stripe_ref text,
status text, -- 'pending'|'captured'|'released'|'failed'
created_at timestamptz DEFAULT now()
);
```
## pgvector
`CREATE EXTENSION vector;` Use for entity matching/dedup and for the AI-assist (semantic match of NL request → catalog/config). Store embeddings in a `embeddings` table keyed by `(entity_type, entity_key)` or alongside catalog rows.
## Row-level security & catalog
- Catalog products: a `catalog` table (or view) over curated datasets with sample rows, counts, coverage, price.
- Per-customer data isolation enforced by `org_id` filtering in the API and (where exposed via NocoDB/Metabase) by scoped views/connection params, plus Postgres RLS where practical.