Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""EPCIS document receipts: fsma.epcis_document_receipts.

Mirrors supabase/migrations/20261024090000_epcis_document_receipts.sql exactly:
same table, columns, constraints, index, RLS policy and grants. One row per
EPCIS document the ingest routes receive, accepted or rejected, keyed by the
SHA-256 of the raw request body. error_summary is a short sanitized summary,
never payload content.

Additive and idempotent (IF NOT EXISTS, drop-if-exists-then-create policy,
role-guarded grants), so the plain SQL file can run ahead of it. Downgrade
drops only what this revision created.

Revision ID: a3f7ce361b0e
Revises: b005ddbf1dba
Create Date: 2026-10-24
"""

from typing import Sequence, Union

from alembic import op

revision: str = "a3f7ce361b0e" # pragma: allowlist secret
down_revision: Union[str, Sequence[str], None] = "b005ddbf1dba" # pragma: allowlist secret
branch_labels = None
depends_on = None

_UPGRADE_STATEMENTS: tuple[str, ...] = (
"""
CREATE TABLE IF NOT EXISTS fsma.epcis_document_receipts (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null,
document_sha256 text not null,
received_at timestamptz not null default now(),
status text not null,
error_summary text,
constraint epcis_document_receipts_status_check
check (status in ('accepted', 'rejected')),
constraint epcis_document_receipts_sha256_check
check (document_sha256 ~ '^[0-9a-f]{64}$'),
constraint epcis_document_receipts_error_summary_length_check
check (error_summary is null or char_length(error_summary) <= 200)
);
""",
"""
-- Read path: rejected share per tenant over a rolling window.
create index if not exists idx_epcis_document_receipts_tenant_received
on fsma.epcis_document_receipts (tenant_id, received_at);
""",
"""
ALTER TABLE fsma.epcis_document_receipts ENABLE ROW LEVEL SECURITY;
""",
"""
ALTER TABLE fsma.epcis_document_receipts FORCE ROW LEVEL SECURITY;
""",
"""
-- CASE form, not OR: public.get_tenant_context() raises when app.tenant_id is
-- unset and Postgres does not order OR operands, so the sysadmin arm is tested
-- first (same predicate shape as alembic v096, v134 and v135).
drop policy if exists tenant_isolation_epcis_document_receipts on fsma.epcis_document_receipts;
""",
"""
create policy tenant_isolation_epcis_document_receipts on fsma.epcis_document_receipts
for all
using (
case
when (select current_setting('regengine.is_sysadmin', true)) = 'true'
and current_user = 'regengine_sysadmin' then true
else tenant_id = (select public.get_tenant_context())
end
)
with check (
case
when (select current_setting('regengine.is_sysadmin', true)) = 'true'
and current_user = 'regengine_sysadmin' then true
else tenant_id = (select public.get_tenant_context())
end
);
""",
"""
-- Runtime role allowlist (alembic v111): new tables are inaccessible to the
-- app role until a migration names them. Insert and select only: a receipt is
-- evidence and is never updated or deleted by the app.
do $$
begin
if exists (select 1 from pg_roles where rolname = 'anon') then
revoke all on fsma.epcis_document_receipts from anon;
end if;
if exists (select 1 from pg_roles where rolname = 'authenticated') then
revoke all on fsma.epcis_document_receipts from authenticated;
end if;
if exists (select 1 from pg_roles where rolname = 'regengine') then
grant select, insert on fsma.epcis_document_receipts to regengine;
end if;
if exists (select 1 from pg_roles where rolname = 'regengine_sysadmin') then
grant select on fsma.epcis_document_receipts to regengine_sysadmin;
end if;
end $$;
""",
)


def upgrade() -> None:
for statement in _UPGRADE_STATEMENTS:
op.execute(statement)


def downgrade() -> None:
op.execute("DROP POLICY IF EXISTS tenant_isolation_epcis_document_receipts ON fsma.epcis_document_receipts")
op.execute("DROP INDEX IF EXISTS fsma.idx_epcis_document_receipts_tenant_received")
op.execute("DROP TABLE IF EXISTS fsma.epcis_document_receipts")
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Agreed sending time per supplier facility.

Mirrors supabase/migrations/20261024080000_supplier_agreed_sending_hours.sql
exactly: adds public.supplier_facilities.agreed_sending_hours, integer NULL,
CHECK (agreed_sending_hours > 0). No backfill. The supplier data quality view
counts a CTE record as late when it is received more than this many hours
after its event time.

Additive and idempotent (ADD COLUMN IF NOT EXISTS with an inline CHECK that is
only created together with the column), so the plain SQL file can run ahead of
it. Downgrade drops only what this revision created.

Revision ID: b005ddbf1dba
Revises: 41f96588d87c
Create Date: 2026-10-24
"""

from typing import Sequence, Union

from alembic import op

revision: str = "b005ddbf1dba" # pragma: allowlist secret
down_revision: Union[str, Sequence[str], None] = "41f96588d87c" # pragma: allowlist secret
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(
"""
alter table public.supplier_facilities
add column if not exists agreed_sending_hours integer null
constraint supplier_facilities_agreed_sending_hours_check
check (agreed_sending_hours > 0);
"""
)


def downgrade() -> None:
op.execute(
"ALTER TABLE public.supplier_facilities DROP CONSTRAINT IF EXISTS supplier_facilities_agreed_sending_hours_check"
)
op.execute("ALTER TABLE public.supplier_facilities DROP COLUMN IF EXISTS agreed_sending_hours")
33 changes: 33 additions & 0 deletions docs/schema/expected_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,35 @@
}
]
},
"fsma.epcis_document_receipts": {
"columns": {
"document_sha256": {
"nullable": false,
"type": "text"
},
"error_summary": {
"nullable": true,
"type": "text"
},
"id": {
"nullable": false,
"type": "uuid"
},
"received_at": {
"nullable": false,
"type": "timestamp"
},
"status": {
"nullable": false,
"type": "text"
},
"tenant_id": {
"nullable": false,
"type": "uuid"
}
},
"foreign_keys": []
},
"fsma.evidence_attachments": {
"columns": {
"created_at": {
Expand Down Expand Up @@ -6807,6 +6836,10 @@
},
"public.supplier_facilities": {
"columns": {
"agreed_sending_hours": {
"nullable": true,
"type": "integer"
},
"city": {
"nullable": false,
"type": "text"
Expand Down
43 changes: 41 additions & 2 deletions docs/security/route-security-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,19 @@
"storage_boundary": "process-observability:no-tenant-data",
"test_adapter": "workload_metrics_key"
},
{
"operation": "admin:PUT:/v1/supplier/admin/facilities/{facility_id}/agreed-sending-hours",
"owner": "admin",
"method": "PUT",
"path": "/v1/supplier/admin/facilities/{facility_id}/agreed-sending-hours",
"classification": "tenant-scoped",
"authentication": "bearer-token-or-opaque-session-cookie",
"required_permissions": [
"tenant.admin"
],
"storage_boundary": "admin-postgres:jwt-tenant-or-session-membership-tenant",
"test_adapter": "tenant_dual_session_csrf"
},
{
"operation": "admin:POST:/v1/supplier/bulk-upload/commit",
"owner": "admin",
Expand Down Expand Up @@ -2852,6 +2865,32 @@
"storage_boundary": "admin-postgres:jwt-tenant-or-session-membership-tenant",
"test_adapter": "tenant_dual_session_csrf"
},
{
"operation": "admin:GET:/v1/supplier/data-quality",
"owner": "admin",
"method": "GET",
"path": "/v1/supplier/data-quality",
"classification": "tenant-scoped",
"authentication": "bearer-token-or-opaque-session-cookie",
"required_permissions": [
"authenticated"
],
"storage_boundary": "admin-postgres:jwt-tenant-or-session-membership-tenant",
"test_adapter": "tenant_dual_session_csrf"
},
{
"operation": "admin:GET:/v1/supplier/data-quality/{facility_id}/records",
"owner": "admin",
"method": "GET",
"path": "/v1/supplier/data-quality/{facility_id}/records",
"classification": "tenant-scoped",
"authentication": "bearer-token-or-opaque-session-cookie",
"required_permissions": [
"authenticated"
],
"storage_boundary": "admin-postgres:jwt-tenant-or-session-membership-tenant",
"test_adapter": "tenant_dual_session_csrf"
},
{
"operation": "admin:POST:/v1/supplier/demo/reset",
"owner": "admin",
Expand Down Expand Up @@ -4918,13 +4957,13 @@
}
],
"summary": {
"enabled_operation_count": 255,
"enabled_operation_count": 258,
"disabled_operation_count": 180,
"disabled_unique_path_count": 163,
"enabled_by_classification": {
"global-sysadmin": 16,
"public": 30,
"tenant-scoped": 202,
"tenant-scoped": 205,
"workload-only": 7
}
}
Expand Down
Loading
Loading