Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions drizzle/0023_subject_portal.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- Subject portal (WP3): consumer self-check + subject-facing portal.
-- Access tokens are stored ONLY as SHA-256 hashes (same scheme as
-- api_tokens."tokenHash"); dispute statements are stored encrypted
-- (Vault Transit envelope) with a SHA-256 integrity digest. No plaintext PII.

BEGIN;

-- consent_purpose gains the self-check purpose used by the subject portal.
ALTER TYPE consent_purpose ADD VALUE IF NOT EXISTS 'consumer_self_check';

CREATE TYPE subject_access_token_purpose AS ENUM ('self_check', 'status', 'dispute');
CREATE TYPE subject_dispute_status AS ENUM ('received', 'under_review', 'resolved');

CREATE TABLE IF NOT EXISTS subject_access_tokens (
id UUID PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
candidate_id INTEGER NOT NULL REFERENCES candidate_profiles(id) ON DELETE RESTRICT,
token_hash TEXT NOT NULL UNIQUE,
purpose subject_access_token_purpose NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS sat_candidate_idx ON subject_access_tokens (tenant_id, candidate_id);
CREATE INDEX IF NOT EXISTS sat_expiry_idx ON subject_access_tokens (expires_at);

CREATE TABLE IF NOT EXISTS subject_disputes (
id UUID PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
candidate_id INTEGER NOT NULL REFERENCES candidate_profiles(id) ON DELETE RESTRICT,
case_id UUID REFERENCES informal_verification_cases(id) ON DELETE RESTRICT,
statement_sha256 CHAR(64) NOT NULL CHECK (statement_sha256 ~ '^[0-9a-f]{64}$'),
statement_enc TEXT,
status subject_dispute_status NOT NULL DEFAULT 'received',
resolution TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS sd_candidate_idx ON subject_disputes (tenant_id, candidate_id);
CREATE INDEX IF NOT EXISTS sd_status_idx ON subject_disputes (tenant_id, status);

COMMIT;
49 changes: 49 additions & 0 deletions drizzle/0024_share_links_and_plan_signups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- 0024_share_links_and_plan_signups.sql
-- Shareable investigation reports (tokenised, expiring, redacted one-pagers)
-- and durable idempotency records for self-service plan signups.

BEGIN;

CREATE TABLE IF NOT EXISTS report_share_links (
id uuid PRIMARY KEY,
tenant_id integer NOT NULL CHECK (tenant_id > 0),
investigation_ref text NOT NULL,
token_hash text NOT NULL,
created_by integer,
expires_at timestamptz NOT NULL,
revoked_at timestamptz,
view_count integer NOT NULL DEFAULT 0 CHECK (view_count >= 0),
last_viewed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS report_share_links_token_hash_idx
ON report_share_links (token_hash);
CREATE INDEX IF NOT EXISTS report_share_links_tenant_idx
ON report_share_links (tenant_id, created_at);
CREATE INDEX IF NOT EXISTS report_share_links_investigation_idx
ON report_share_links (tenant_id, investigation_ref);

COMMENT ON TABLE report_share_links IS
'Expiring share links for redacted investigation one-pagers; only the SHA-256 digest of the bis_sl_ token is stored, never the plaintext token.';

CREATE TABLE IF NOT EXISTS plan_signups (
id uuid PRIMARY KEY,
tenant_id integer NOT NULL CHECK (tenant_id > 0),
plan_code text NOT NULL,
status text NOT NULL CHECK (status IN ('active', 'payment_failed', 'cancelled')),
billing_ref text,
idempotency_key text NOT NULL,
created_by integer,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Idempotency keys are tenant-namespaced: per-tenant uniqueness prevents both
-- cross-tenant replay leaks and cross-tenant key squatting.
CREATE UNIQUE INDEX IF NOT EXISTS plan_signups_idempotency_key_unique
ON plan_signups (tenant_id, idempotency_key);
CREATE INDEX IF NOT EXISTS plan_signups_tenant_idx
ON plan_signups (tenant_id, created_at);

COMMENT ON TABLE plan_signups IS
'Durable, tenant-scoped idempotency records for self-service plan signups; a replayed idempotency key returns the original result and never re-settles payment.';

COMMIT;
14 changes: 14 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@
"when": 1788631200000,
"tag": "0022_payment_reconciliation_cases",
"breakpoints": true
},
{
"idx": 23,
"version": "7",
"when": 1788634800000,
"tag": "0023_subject_portal",
"breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1788638400000,
"tag": "0024_share_links_and_plan_signups",
"breakpoints": true
}
]
}
Loading