Skip to content

feat(epcis): document receipts and live EPCIS failure metric - #3576

Merged
PetrefiedThunder merged 15 commits into
claude/supplier-data-qualityfrom
claude/epcis-document-receipts
Oct 25, 2026
Merged

PetrefiedThunder merged 15 commits into
claude/supplier-data-qualityfrom
claude/epcis-document-receipts

Conversation

@PetrefiedThunder

@PetrefiedThunder PetrefiedThunder commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

STACKED PR

Based on claude/supplier-data-quality (#3574), which is stacked on #3572. Retarget this PR to main after #3574 and #3572 merge.

Do not merge until C.Lee approves the production migration.

What changed

New table fsma.epcis_document_receipts (alembic a3f7ce361b0e v136, down revision 41f96588d87c; mirror supabase/migrations/20261024070000_epcis_document_receipts.sql).
One row per EPCIS document an ingest route receives, accepted or rejected. Columns: id, tenant_id, document_sha256 (SHA-256 of the raw request body), received_at, status (accepted or rejected), error_summary. Index on (tenant_id, received_at). RLS enabled and forced, one tenant isolation policy in the v096/v134/v135 CASE form (sysadmin arm first, then tenant_id = public.get_tenant_context(), which reads app.tenant_id). Grants: regengine SELECT and INSERT only (receipts are evidence; no UPDATE or DELETE), regengine_sysadmin SELECT, anon and authenticated revoked. Two extra checks beyond the brief: document_sha256 must be 64 lowercase hex, error_summary at most 200 characters.

Receipts on every EPCIS ingest (services/ingestion/app/epcis/router.py, new services/shared/epcis_receipts.py).
POST /api/v1/epcis/events, /events/batch and /events/xml now file one receipt per request body that reaches the handler:

  • success (201/200) is accepted;
  • a partial batch with some failed events (207) is accepted, summary N of M events failed;
  • HTTP 400 and up, any raised HTTPException (validation failures included) and any unexpected exception are rejected.

error_summary holds the exception class, the HTTP status and, for a dict detail, its error code when it matches ^[a-z][a-z0-9_]{0,63}$ (for example HTTPException 422 fsma_validation_failed). str(exc) and string details are never stored, because they can quote payload values. Truncated to 200 characters.
The receipt is written in its own session and transaction (set_tenant_guc then INSERT). record_epcis_receipt never raises: a receipt failure is logged with the exception class only and the ingest response is unchanged. The batch and XML handler bodies moved unchanged into _ingest_batch_document and _ingest_xml_document so one wrapper can file the receipt.

Requests that get no receipt, because there is no complete document or no tenant:

  • auth failure (no tenant context);
  • a body FastAPI rejects before the handler runs (not valid JSON for /events or /events/batch);
  • /events/xml with an invalid mode or a body over the size cap (rejected before the body is read);
  • a non UUID tenant id (the column is uuid).

POST /api/v1/epcis/validate stores nothing and files no receipt.

epcis_failed metric is live (services/shared/supplier_data_quality.py).
Rejected receipts divided by all receipts for the tenant in the window. Under MIN_RECORDS (20) receipts the status is not_enough_data and numerator and denominator are still returned.
Facility mapping: it cannot be mapped per facility. An EPCIS upload is authenticated per tenant and carries no supplier facility, so receipts are tenant scoped. The tenant wide share is computed once and repeated on every facility, and the metric reason says so: "Counted for your whole account, not for this facility: an EPCIS upload is not linked to a facility, so every facility shows the same count." The count runs in a savepoint; if the table cannot be read (for example before this migration is applied) the metric is unavailable with reason "EPCIS document receipts could not be read." and the other three metrics are unaffected.

UI (frontend/src/app/dashboard/suppliers/data-quality.tsx). The label is now "EPCIS documents rejected" (unit "documents"). A computed metric that carries a reason shows it under the value, so the tenant wide note is visible.

Migration SQL (up)

-- EPCIS document receipts.
--
-- One row per EPCIS document the ingest routes receive (POST
-- /api/v1/epcis/events, /events/batch and /events/xml), accepted or rejected.
-- document_sha256 is the SHA-256 of the raw request body. error_summary is a
-- short sanitized summary (exception class, HTTP status, error code or failed
-- event count); it never holds payload content or secrets.
--
-- Receipts are tenant scoped. An EPCIS upload is not linked to a supplier
-- facility, so the supplier data quality metric epcis_failed is a tenant wide
-- share shown on every facility.
--
-- Idempotent: safe to re-run. Downgrade block at the end of this file.

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 $$;

Downgrade SQL

DROP POLICY IF EXISTS tenant_isolation_epcis_document_receipts ON fsma.epcis_document_receipts;
DROP INDEX IF EXISTS fsma.idx_epcis_document_receipts_tenant_received;
DROP TABLE IF EXISTS fsma.epcis_document_receipts;

Alembic downgrade() runs the same three statements.

Read only verification SQL (for C.Lee, after applying)

-- 1. Alembic head
SELECT version_num FROM alembic_version;  -- expect a3f7ce361b0e

-- 2. Columns
SELECT column_name, data_type, is_nullable, column_default
  FROM information_schema.columns
 WHERE table_schema = 'fsma' AND table_name = 'epcis_document_receipts'
 ORDER BY ordinal_position;

-- 3. Constraints (status, sha256, error_summary length)
SELECT conname, pg_get_constraintdef(oid)
  FROM pg_constraint
 WHERE conrelid = 'fsma.epcis_document_receipts'::regclass
 ORDER BY conname;

-- 4. Index
SELECT indexname, indexdef
  FROM pg_indexes
 WHERE schemaname = 'fsma' AND tablename = 'epcis_document_receipts';

-- 5. RLS enabled and forced (expect t, t)
SELECT relrowsecurity, relforcerowsecurity
  FROM pg_class
 WHERE oid = 'fsma.epcis_document_receipts'::regclass;

-- 6. Policy (expect one row, tenant_isolation_epcis_document_receipts, cmd *)
SELECT polname, polcmd, polpermissive, polroles::regrole[],
       pg_get_expr(polqual, polrelid) AS using_expr,
       pg_get_expr(polwithcheck, polrelid) AS check_expr
  FROM pg_policy
 WHERE polrelid = 'fsma.epcis_document_receipts'::regclass;

-- 7. Grants (expect regengine INSERT, SELECT; regengine_sysadmin SELECT; nothing for anon or authenticated)
SELECT grantee, privilege_type
  FROM information_schema.role_table_grants
 WHERE table_schema = 'fsma' AND table_name = 'epcis_document_receipts'
 ORDER BY grantee, privilege_type;

-- 8. After some EPCIS traffic (counts only, no payload is stored)
SELECT tenant_id, status, count(*)
  FROM fsma.epcis_document_receipts
 WHERE received_at >= now() - interval '1 day'
 GROUP BY tenant_id, status
 ORDER BY tenant_id, status;

Tests

Tests are data free: ids and payload values come from uuid4 and crypto.randomUUID, counts come from generated input lengths and MIN_RECORDS.

  • services/shared/tests/test_epcis_receipts.py (new): 20 passed. Hashing, summary sanitizing (string details and non code error values dropped, truncation), writer sets the tenant GUC then inserts and commits, never raises on insert failure, session failure or missing session, skips non UUID tenants and unknown statuses; count reader is tenant and window scoped and returns None when the table is unreadable.
  • services/shared/tests/test_supplier_data_quality.py: 39 passed. Live tenant wide share with reason, not_enough_data under MIN_RECORDS, unavailable when unreadable, same counts on every facility from one tenant scoped query, no dashes in the reasons.
  • services/ingestion/tests/test_epcis_document_receipts.py (new): 14 passed. Accepted and rejected receipts for single, atomic batch, partial batch (207 and all failed) and XML; receipt SHA-256 equals the raw body hash; payload values never appear in the summary; a failing receipt write leaves both a 201 and a 422 response unchanged; invalid XML mode files no receipt.
  • All EPCIS ingestion tests services/ingestion/tests/test_epcis_*.py: 333 passed.
  • services/admin/tests/test_supplier_data_quality_routes.py: 14 passed. Live share and reason in the route response; receipts counted once per tenant with no user or facility filter; not_enough_data and unavailable paths.
  • services/admin/tests/test_supplier_compliance_routes_1342.py with the above: 22 passed.
  • tests/migrations/test_alembic_chain_health.py and tests/scripts/test_check_alembic_revisions.py: 21 passed (single head is a3f7ce361b0e).
  • Frontend src/__tests__/components/supplier-data-quality.test.tsx: 11 passed; with suppliers-metrics.test.tsx and suppliers-removal.test.tsx: 17 passed.
  • npx tsc --noEmit -p .: only the existing src/instrumentation.ts(26,5) error.
  • ruff check and ruff format clean on every touched Python file; eslint clean on touched frontend files.

Environment notes: the ingestion tests ran with the main checkout venv, DATABASE_URL=sqlite:// (no database contacted) and a stub for the missing typesafe_sdk import. tests/security/test_route_security_manifest.py could not load the full app locally (llama_cloud is not installed); no route path, method or dependency changed, so the manifest should not move, but that is not verified locally. The migration has not been run against any database.

Ordering with #3575

This PR and #3575 (agreed sending hours) both chain their Alembic revision to 41f96588d87c and both use the Supabase timestamp 20261024070000. Whichever merges second must re-chain its down_revision to the other revision (b005ddbf1dba or a3f7ce361b0e) and bump its Supabase file timestamp so there is a single Alembic head.


Note

Medium Risk
Touches production schema/RLS and all main EPCIS ingest paths; receipt persistence is fail-open, but migration ordering and ingest wrappers still need careful rollout.

Overview
Adds fsma.epcis_document_receipts (Alembic v137, Supabase mirror, expected_schema.json) to record one accepted/rejected row per EPCIS ingest document, keyed by SHA-256 of the raw body, with RLS, tenant index, and insert-only grants for regengine.

Ingestion (epcis/router.py + new shared/epcis_receipts.py) files a receipt on single event, batch, and XML routes after the handler runs: HTTP ≥400 and exceptions → rejected with a sanitized error_summary (no payload text); 207 partial batches → accepted with an event-failure count. Writes are fail-open so receipt DB errors never change the HTTP response.

Supplier data quality turns epcis_failed from unavailable into a tenant-wide rejected-document share over the rolling window (one count query, repeated on every facility with EPCIS_TENANT_SCOPE_REASON). The dashboard updates copy, shows that reason, and keeps EPCIS out of drill-down.

Tests cover receipt hashing/summaries, route filing behavior, metric edge cases, and API responses.

Reviewed by Cursor Bugbot for commit a9e7bb2. Bugbot is set up for automated code reviews on this repo. Configure here.

…metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
…re metric

Adds fsma.epcis_document_receipts with forced RLS and tenant policies (Alembic and Supabase migrations with downgrades), writes an accepted or rejected receipt with a sanitized error summary from the EPCIS ingest routes, and computes the EPCIS failure metric.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
regengine Ready Ready Preview Oct 25, 2026 4:46am UTC

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 1
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T08:14:19.532149Z b64bf33 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b64bf33808

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +204 to +207
receipt_tenant = _.tenant_id if _ is not None else None
raw_body = await request.body() if receipt_tenant else b""
try:
response = _ingest_batch_document(request, body, mode, x_tenant_id, x_regengine_api_key, _)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record receipts for request-model validation failures

For an authenticated batch upload such as {"events":[null]}, FastAPI rejects BatchIngestRequest with HTTP 422 before this handler runs, so neither receipt-writing branch executes. The single-event route has the same gap for bodies that fail its dict validation. The installed shared.error_handling._validation_exception_handler only returns an error response and does not record a receipt. Consequently, these rejected documents disappear from both the receipt evidence and epcis_failed, understating the rejection share. Capture authenticated request-validation failures at the route wrapper or exception-handler layer while preserving the existing 422 response.

AGENTS.md reference: AGENTS.md:L56-L56

Useful? React with 👍 / 👎.

@PetrefiedThunder PetrefiedThunder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta review of #3576 against claude/supplier-data-quality (4332a82) at b64bf33.

Code: sound.

  • RLS: ENABLE and FORCE are both set. The policy is FOR ALL, uses the CASE form, and is tenant-scoped in both USING and WITH CHECK. anon and authenticated have everything revoked; regengine gets select and insert; regengine_sysadmin gets select. The Supabase and Alembic versions match, and the downgrade drops the policy, the index and the table.
  • error_summary: summarize_error uses only the exception class, an int status, and a ^[a-z][a-z0-9_]{0,63}$ error code. str(exc) and string details are never used. The failed-event summary takes integer counts from the response, and the column is length-capped at 200 by a CHECK and by truncation. No payload or secrets can get in.
  • Fail-open: record_epcis_receipt catches everything, rolls back and returns False. The routes re-raise the original exception, and response receipts are filed after the response is built. A receipt failure can't change the response.
  • Metric: the tenant-wide rejected/total share since the window start, read in a savepoint, and unavailable only when the table can't be read. The tenant-wide scope is stated in the reason.
  • Non-blocking nit: services/ingestion/app/epcis/router.py:107-108 could check that failed and total are ints before formatting.

FAIL: invented counts in the tests (the same standard as #3574 round 2):

  • services/ingestion/tests/test_epcis_document_receipts.py:162 range(3), :178 range(2), :189 range(4), :207 range(2), :241 range(2). These are arbitrary event counts.
  • services/ingestion/tests/test_epcis_document_receipts.py:190 {events[0]["id"], events[2]["id"]} picks arbitrary indexes.

Fix: size these from a spec constant (for example MIN_RECORDS, as the other test files do), and pick the failing events by slicing (events[::2]), so counts come from the inputs.

CI: Vercel Agent Review, Vercel Preview Comments and CodeRabbit pass. The rest is pending, and no backend suite has run on this stacked base. No migration was applied.

REVIEW: FAIL codex b64bf33

…counts are ints

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
…CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

@PetrefiedThunder PetrefiedThunder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta review of #3576: b64bf33 -> b6908a0.

  • All five batches in services/ingestion/tests/test_epcis_document_receipts.py (:165, :181, :192, :210, :244) use range(BATCH_SIZE) with BATCH_SIZE = MIN_RECORDS (:38).
  • The failing set is {event["id"] for event in events[::2]}, and the expected summary comes from len(failing) and len(events).
  • services/ingestion/app/epcis/router.py: total is read only when the content is a dict, and a summary is built only when failed and total are both ints.
  • The new parametrized test uses only BATCH_SIZE, str(BATCH_SIZE), None and a uuid4 body, so there are no invented literals.

My earlier findings are fixed, and everything else is as in my last review.

CI: Bugbot, Vercel Agent Review, Vercel Preview Comments, Vercel and CodeRabbit pass; Non-main PR Secret Scan and report are pending. No backend suite has run on this stacked base, so merging needs green backend CI after it is retargeted to main. No migration was applied.

REVIEW: PASS codex b6908a0

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Blocker Checklist

Head SHA: a9e7bb27e4631cc80f0d2b2244a0928502f26dc3

Status: Blocked (2 open blockers)

Review Threads (1 unresolved)

Changes Requested (0)

  • No reviews requesting changes

Blocked Engines (1)

  • Engine codex blocked at b64bf33 (current head: a9e7bb2): View verdict: REVIEW: FAIL codex b64bf33808c9760ace556fc6b317ec17dfe74fdf

…r-agreed-sending-hours; move Supabase migration to 20261024080000

Alembic b005ddbf1dba already revises main head 41f96588d87c. The Supabase file
moves from 20261024070000 to 20261024080000 so it no longer collides with

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
#3576 and sorts after 20261024060000 (data_gap_time_to_fix); #3576 takes 20261024090000.
…ocument-receipts; rechain migration after #3575

Alembic a3f7ce361b0e now revises b005ddbf1dba (#3575, supplier agreed sending
hours), which revises main head 41f96588d87c, giving one linear head. File
renamed v136 -> v137. Supabase file moves 20261024070000 -> 20261024090000 to
sort after #3575 (20261024080000). Requires #3575 to merge first.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@PetrefiedThunder

Copy link
Copy Markdown
Collaborator Author

REVIEW: FAIL deepseek 39d7c2e

Reviewed by hand: Mac load was 34–46, over the 28 limit, so no DeepSeek pass.
Blocking:

  • alembic/versions/20261024_a3f7ce361b0e_v137_epcis_document_receipts.py:29 / supabase/migrations/20261024090000_epcis_document_receipts.sql:15 create fsma.epcis_document_receipts, but docs/schema/expected_schema.json at this head doesn't include it. The schema parity audit will fail with an EXTRA TABLE error once the KeyError is gone. Regenerate the snapshot in this PR (or in feat(suppliers): agreed sending hours and live late CTE metric #3575 after both are stacked).
  • The KeyError isn't the only expected CI blocker (see above). At this head the KeyError-producing job hasn't run yet (security, tenant-isolation and SAST are queued), so I couldn't confirm its cause from CI. Structurally it's expected: down_revision b005ddbf1dba (line 23) isn't on the base until feat(suppliers): agreed sending hours and live late CTE metric #3575 lands.
    Verified OK:
  • Chain main -> b005ddbf1dba -> a3f7ce361b0e: a single head with no missing parents. No collisions with main's revisions or Supabase timestamps (090000 is unused).
  • Merge delta: rechaining 41f96588d87c -> b005ddbf1dba, renaming v136 -> v137 and moving Supabase 070000 -> 090000 are all documented in the commit. No stale v136/070000 references. The alembic SQL matches the Supabase SQL once normalized.
  • Schema-only migration (table, RLS, role-guarded grants). The INSERT in services/shared/epcis_receipts.py is the runtime receipt write, not seed data. No synthetic data in non-test code.

PetrefiedThunder and others added 6 commits October 25, 2026 02:59
* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
… ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
…-receipts

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Comment thread scripts/copy-lint/baseline.json Resolved

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 730ba95. Configure here.

…p walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
…-receipts

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@PetrefiedThunder

Copy link
Copy Markdown
Collaborator Author

REVIEW: PASS deepseek f9debff

Delta review of f9debff (merge of c77c400 / #3575 into 730ba95). Reviewed by hand (load was 23.9, under the limit, but a manual pass was enough for this delta).

  • docs/schema/expected_schema.json:1477 adds fsma.epcis_document_receipts: id uuid NOT NULL, tenant_id uuid NOT NULL, document_sha256 text NOT NULL, received_at timestamp NOT NULL (timestamptz is written as timestamp, the same as the rest of the snapshot), status text NOT NULL, error_summary text NULL, and foreign_keys []. This matches alembic/versions/20261024_a3f7ce361b0e_v137_epcis_document_receipts.py:29-42 and supabase/migrations/20261024090000_epcis_document_receipts.sql, which have the same DDL. agreed_sending_hours from feat(suppliers): agreed sending hours and live late CTE metric #3575 is kept at :6839. This fixes the earlier FAIL at 39d7c2e.
  • Alembic chain: a3f7ce361b0e has down_revision b005ddbf1dba (:23), and b005ddbf1dba has down_revision 41f96588d87c. b005ddbf1dba has exactly one child. Parsing every file in alembic/versions gives a single head, a3f7ce361b0e.
  • The Supabase file is 20261024090000_epcis_document_receipts.sql, after 20261024080000_supplier_agreed_sending_hours.sql.
  • Merge resolution: frontend/src/app/dashboard/suppliers/data-quality.tsx:20 is resolved to "EPCIS (Preview) documents rejected" with unit "documents". That fits the receipts metric, and the component test expects that exact label. The feat(suppliers): agreed sending hours and live late CTE metric #3575 copy (ingest/page.tsx:695 and walkthrough:67) and the baseline came through. The baseline matches c77c400 exactly, with no count increases.
  • Real data: the migrations and prod code add no seed rows, test facilities or synthetic values. error_summary is capped at 200 characters and sanitized.
  • CI: Copy gate passed, and so did the security, tenant-isolation and PR Blocker Checklist checks. The only failures are the Engine Review Pass and Head SHA Check Guard review gates. No backend or schema-parity job ran for this PR (base is not main). Parity was checked by hand above, and it must go green on the PR to main. Merge is on hold for C.Lee.

Keeps the tenant admin lock from #3584 (admin only PUT, read only supplier
view), runtime emails from #3587, and the tenant wide EPCIS (Preview)
receipt metric from this PR. The epcis_failed label now says it counts
across the whole account. Route manifest drops the old supplier PUT.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@PetrefiedThunder

Copy link
Copy Markdown
Collaborator Author

REVIEW: PASS deepseek a9e7bb2

This is a delta review from f9debff. I reviewed by hand because the Mac's load was 35 to 99, over the 28 limit, so DeepSeek wasn't run and I didn't run pytest, the manifest --check or the copy gate locally.

Merge resolution, diffed against base claude/supplier-data-quality (886eb63):

  • No supplier write path came back. The only agreed-sending-hours write is still the admin-only PUT /admin/facilities/{facility_id}/agreed-sending-hours (services/admin/app/supplier_compliance_routes.py:383, with the 403 at :376). The manifest has only that admin:PUT entry (docs/security/route-security-manifest.json:2778). In the UI, data-quality.tsx:230 passes agreedSendingHours to the read-only SendingHoursNote, and no editor came back. The PR doesn't touch the manifest against the base.
  • The tenant scoping from fix(suppliers): lock agreed sending hours to tenant admins #3584 is intact. The record and lot queries still filter on tenant, user and facility (shared/supplier_data_quality.py). The new fetch_receipt_counts is tenant-only, which is documented (:24-34, route docstring :271-273) and asserted at test_supplier_data_quality_routes.py:184-186. There's a new early return if not facility_ids: return {}, which is fine.
  • The real-membership tests and _runtime_email are intact (test_supplier_data_quality_routes.py:408, :437-438) and not stubbed. The _get_user_role_name stub at :119 is the existing _client helper from the base (:105 there), not a new one.
  • Migrations: there's a single Alembic head, a3f7ce361b0e, whose down_revision is b005ddbf1dba (the base head, agreed_sending_hours). Supabase: 20261024080000_supplier_agreed_sending_hours.sql comes before 20261024090000_epcis_document_receipts.sql. expected_schema.json has entries for both agreed_sending_hours and fsma.epcis_document_receipts.
  • The label (data-quality.tsx:19) reads "EPCIS (Preview) documents rejected across your whole account". It has no em or en dash, the reasons are checked dash-free by a test, and the PR doesn't touch any copy-lint baseline file.
  • The tests are data-free: counts come from runtime uuid4 lists.

CI at posting:

  • Passing: CodeRabbit, Bugbot and Vercel.
  • Still pending: Copy gate, the secret scan, the security path checks and the other gates.
  • Failing: Engine Review Pass ("No passing review from a non-author engine for current head SHA"). Head SHA Guard and the PR Blocker Checklist also failed, but their logs aren't retrievable and they look like the same review gates. They should re-run after this review.

This PASS covers the code. Merge only once the Copy gate and the pending checks are green.

@PetrefiedThunder
PetrefiedThunder merged commit 2d1abbf into claude/supplier-data-quality Oct 25, 2026
30 of 35 checks passed
@PetrefiedThunder
PetrefiedThunder deleted the claude/epcis-document-receipts branch October 25, 2026 05:03
PetrefiedThunder added a commit that referenced this pull request Oct 25, 2026
* feat(suppliers): track data gap time to fix per supplier

Open one gap event per flagged missing KDE or failed CTE validation, close it only when a passing record for the same lot and CTE arrives, and store a receipt hash of the closing record. Adds a per supplier view with open gaps, median time to fix, oldest open gap and a 24 hour at risk flag citing 21 CFR 1.1455(c)(1). Migration included but not applied.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(alembic): add v135 data_gap_events revision and redact gap write errors

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(suppliers): flag gaps open at a records request response due time

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(suppliers): add per supplier data quality view

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(suppliers): generate ids at runtime in data quality tests

Replace hand written UUIDs, record ids, lot codes and facility ids with uuid4 and crypto.randomUUID values, and assert on values derived from the generated inputs.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(suppliers): derive data quality test counts from generated inputs

Counts now come from lengths of generated id arrays or from MIN_RECORDS, DEFAULT_WINDOW_DAYS and MAX_WINDOW_DAYS. KDE values, reasons, tenant and user ids are generated. Removes the Date.now based numerator.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(suppliers): agreed sending hours and live late CTE metric (#3575)

* feat(suppliers): agreed sending hours per facility and live late CTE metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(security): count the agreed sending hours PUT as a dual session CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): real-data follow-up to #3471 (v3) (#3544)

* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): Engine Review Pass picks up real PASS markers (#3577)

* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* feat(copy): SEL-452 copy gate with capabilities registry and baseline ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(schema): snapshot supplier_facilities.agreed_sending_hours (v136)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): label EPCIS as Preview in supplier quality and ingest, drop walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(suppliers): lock agreed sending hours to tenant admins (#3584)

* feat(suppliers): agreed sending hours per facility and live late CTE metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(security): count the agreed sending hours PUT as a dual session CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): real-data follow-up to #3471 (v3) (#3544)

* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): Engine Review Pass picks up real PASS markers (#3577)

* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* feat(copy): SEL-452 copy gate with capabilities registry and baseline ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(schema): snapshot supplier_facilities.agreed_sending_hours (v136)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): label EPCIS as Preview in supplier quality and ingest, drop walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(suppliers): lock agreed sending hours to tenant admins

Suppliers no longer set their own late CTE threshold. The write moves to
PUT /v1/supplier/admin/facilities/{facility_id}/agreed-sending-hours,
gated by the tenant Owner or Admin role check from tenant settings (#1386)
and scoped to facilities in the caller's tenant. The supplier dashboard
shows the agreed time read only.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(suppliers): scope facility lookup in query and test real tenant admin membership

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* test(suppliers): generate test emails at runtime (#3587)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(epcis): document receipts and live EPCIS failure metric (#3576)

* feat(suppliers): agreed sending hours per facility and live late CTE metric

Adds supplier_facilities.agreed_sending_hours (nullable, positive) with Alembic and Supabase migrations and downgrades, a tenant scoped PUT route and dashboard control, and computes the late CTE metric against the agreed sending time.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* feat(epcis): record a receipt for every EPCIS document and live failure metric

Adds fsma.epcis_document_receipts with forced RLS and tenant policies (Alembic and Supabase migrations with downgrades), writes an accepted or rejected receipt with a sanitized error summary from the EPCIS ingest routes, and computes the EPCIS failure metric.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(epcis): size receipt batches from MIN_RECORDS and check partial counts are ints

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* test(security): count the agreed sending hours PUT as a dual session CSRF mutation

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): real-data follow-up to #3471 (v3) (#3544)

* fix(dashboard): drop fabricated demo data from recall report page

Remove the hardcoded demo investigation stats and affected-lot rows
(invented suppliers, lot codes, consumer counts). Demo mode now renders
the same No data yet empty state with a link to import records.

* feat(copy): label every EPCIS intake and export mention as (Preview)

EPCIS is preview on both paths, so feature-name mentions across app and
marketing copy now carry the (Preview) marker. The integrations eyebrow
now reads CSV upload, JSON events API, EPCIS (Preview). The FDA sortable
export stays unlabeled. Stored keys and option values are unchanged.

* fix(copy): keep typed EPCIS format value unlabeled in customer-readiness

The format field is a string union used for logic; the (Preview) marker
stays on the display name only.

* feat(pricing): show honest unavailable state when billing is unreachable

Drop the hardcoded tier prices and the silent fallback. Prices now come
only from the billing service; when it cannot be reached the page renders
a Pricing is temporarily unavailable state with signup and contact links
instead of invented figures. Hardcoded dollar amounts in the pricing copy
and metadata are removed as well. EPCIS 2.0 export carries the (Preview)
label; the FDA sortable export stays unlabeled.

* fix(app): remove sample and sandbox banners and the simulated ingest walkthrough

Dashboard shell and DemoBanner now say No data yet with a link to connect
real data on the Ingest page. The ingest page no longer fakes a job for
the first example URL; it submits for real. The Sample: Daily FDA package
archive entry is removed from the archive jobs list. Test assertions
updated for the EPCIS (Preview) status labels.

* fix(data-import): remove synthetic sample dataset simulation

Delete SAMPLE_EVENTS, the handleLoadSample flow with its state and types,
and the Load Sample Dataset / Run Simulation card. A regression test
asserts the control is not rendered. Real CSV upload and the API guide
are unchanged.

* test(copy): assert csv-sftp display text never renders SFTP

Display labels were already clean (CSV Import); this locks the invariant
for the csv-sftp registry entry name, customer copy, and the
csv_sftp_import delivery-mode label. Stored keys and ids are unchanged.

* fix(receiving): use a comma in generated Receiving Dock location names

New receiving submissions name the location 'Receiving Dock, <supplier>'
instead of using a dash. No existing records are touched.

* fix(inflow-lab): hide Inflow Lab links from dashboard, docs, ingest, integrations and product

* fix(tools): remove Inflow Lab simulator, walkthrough sample payloads and recall drill demo data

Drops tools/inflow-lab components, lib/api-samples ingest walkthrough JSON, synthetic tools/ask and tools/export copy, and invented proof/recall-drill numbers. Recall report shows the #3536 no_data empty state.

* ci: trigger required gates

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ci): Engine Review Pass picks up real PASS markers (#3577)

* fix(ci): Engine Review Pass reads issue comment markers and allows pi, deepseek, devops

Real PASS markers on #3553, #3547, #3530 and #3527 were ignored: pi was not an
allowed engine, issue comment markers were never read, and devops/ branches
failed as an invalid author engine. Also rerun on PR issue comments.

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(engine-review-pass): drop devops, add hermes, require exact full sha

Signed-off-by: Christopher Sellers <1@christophersellers.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* feat(copy): SEL-452 copy gate with capabilities registry and baseline ratchet (#3549)

* feat(copy): SEL-452 copy gate with four-status capabilities registry and baseline ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* ci(copy): copy-gate path filters include copy-gate.yml

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy): gate against main's baseline so a PR cannot loosen the ratchet

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* fix(copy-gate): reject unparseable registry, fail on baseline lookup errors, diff pushes against event.before

* chore(copy-lint): refresh baseline after main removed sandbox copy (SEL-452)

Signed-off-by: Christopher Sellers <1@christophersellers.com>

* chore(copy-gate): refresh copy baseline after merging main (counts only dropped)

* fix(schema): include public.data_gap_events in expected schema snapshot

Regenerate docs/schema/expected_schema.json from a fresh alembic upgrade head so the RLS isolation suite schema parity check stops reporting EXTRA TABLE for the v135 data gap migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(schema): snapshot supplier_facilities.agreed_sending_hours (v136)

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

* fix(copy): label EPCIS as Preview in supplier quality and ingest, drop walkthrough dash

Refresh copy-lint baseline.json so it matches current counts.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Signed-off-by: Christopher Sellers <1@christophersellers.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

This branch was successfully deployed

1 active deployment
Preview — a9e7bb27 Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant