feat(034): distinguish ingestion types (invoice vs license request)#111
feat(034): distinguish ingestion types (invoice vs license request)#111studert wants to merge 5 commits into
Conversation
…ypes Analyses how invoice ingestion and license-request ingestion currently share one ingestion_log table, history table, activity feed and settings tab via column overloading, and proposes a kind/source discriminator plus a JSONB details payload and an ingestion-type registry to make each type self-describing and future ingestions cheap to add.
Resolves all five open questions from the proposal with the default answers (sync stays separate, filter+sub-tabs, no new outcome enum, user_import reserved, JSONB details). Specifies the expand/migrate/ contract schema phases, the type registry, write/read path changes, a file-by-file map and phased task list. UI work uses the in-repo Nothing design system; the plan document itself is rendered in Nothing style (Doto/Space Grotesk/Space Mono, OLED dark).
Implements the expand/migrate phases of the ingestion-types plan so the two ingestion kinds stop sharing one invoice-shaped record. Schema (P0, additive — migration 0023): - new ingestion_kind / ingestion_source_type enums - ingestion_log gains kind, source_type, label, entity_type, entity_id, details(jsonb) + kind index; deprecated invoice columns retained - IngestionDetails discriminated union in src/types/ingestion.ts Write path (P2): - logIngestion() canonical discriminated logger; logIngestionAttempt() kept as a dual-writing deprecated shim so invoice call sites are untouched and legacy columns stay populated during the window - license-request route stops overloading linked_invoice_id and the 'filtered' outcome; dedup is success + details.deduped (closes the latent cross-type FK bug) Read path + UI (P3): - getIngestionHistory returns kind/details/entity - ingestion-type registry (icon, drill-through, label) + server-safe label builder - history table rebuilt: Kind pill, Summary, per-kind drill-through, All/<kind> sub-tabs; Nothing-design primitives - generalised settings + activity-feed copy Backfill (P1): scripts/backfill-ingestion-kind.ts (idempotent, dry-run by default) classifies legacy rows by form_response_id. Tests: +10 unit tests; typecheck + lint clean; 394 existing tests pass. Migration not applied here (no DB in env); P4 column drop deferred.
getRecentDashboardActivity now branches on ingestion kind: invoices read
as 'Invoice from {vendor}', license requests as 'License request ·
{requester}' with tool/tier or duplicate/failed detail. Sourced from the
new details payload with legacy columns as fallback for un-backfilled
rows; pre-034 'filtered' dedup rows render the same as success+deduped.
Resolves the activity-feed follow-up from the implementation notes.
typecheck + lint clean; 404 tests pass.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
There was a problem hiding this comment.
Pull request overview
This PR makes ingestion events first-class by distinguishing invoice ingestion vs. license-request ingestion throughout the schema, write/read paths, and admin UI. It introduces a kind discriminator plus JSONB details, replaces the overloaded linked_invoice_id usage with a polymorphic (entity_type, entity_id) drill-through reference, and updates Settings → Ingestion + the dashboard activity feed to render kind-appropriate summaries and links.
Changes:
- Add new ingestion enums/columns (
kind,source_type,label,entity_*,details) via additive migration0023. - Introduce a canonical
logIngestion()(with an invoice-shaped deprecated shim) + label/registry helpers and unit tests. - Update license-request ingest logging, ingestion history table UI, and dashboard activity feed to be kind-aware.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/ingestion/labels-and-registry.test.ts | Adds unit tests for label building, registry drill-through, and present-kinds ordering. |
| src/types/ingestion.ts | Defines ingestion type system (kinds/source/outcome/channel + details union). |
| src/types/index.ts | Re-exports ingestion types from the central types barrel. |
| src/lib/ingestion/registry.tsx | Adds kind registry mapping kinds to icons and drill-through behavior. |
| src/lib/ingestion/labels.ts | Adds server-safe kind labels and buildIngestionLabel() used to populate ingestion_log.label. |
| src/lib/ingestion-logger.ts | Implements canonical discriminated ingestion logger + deprecated invoice shim. |
| src/lib/db/schema.ts | Adds new ingestion enums/columns and index; keeps legacy invoice columns for migration window. |
| src/lib/db/migrations/meta/_journal.json | Registers migration 0023_perfect_runaways. |
| src/lib/db/migrations/0023_perfect_runaways.sql | Adds enums + new ingestion_log columns + kind index (additive migration). |
| src/components/dashboard/admin/activity-timeline.tsx | Generalizes the activity card description copy to cover non-invoice ingestions. |
| src/app/settings/ingestion/page.tsx | Updates ingestion settings page copy to mention license requests. |
| src/app/settings/ingestion/ingestion-history-table.tsx | Rebuilds ingestion history UI to show Kind pill + Summary + per-kind drill-through + kind sub-tabs. |
| src/app/api/license-requests/ingest/route.ts | Switches license-request ingestion logging to logIngestion() with correct kind/details/entity and dedup semantics. |
| src/actions/ingestion-log.ts | Updates ingestion-history query/result shape to include kind/source/label/details/entity fields. |
| src/actions/dashboard.ts | Makes recent dashboard ingestion activity kind-aware using kind/details/label with legacy fallbacks. |
| specs/034-ingestion-types-distinction/proposal.html | Adds design proposal document for ingestion type distinction. |
| specs/034-ingestion-types-distinction/implementation-plan.html | Adds implementation plan document. |
| specs/034-ingestion-types-distinction/implementation-notes.html | Adds implementation notes / decisions log. |
| scripts/backfill-ingestion-kind.ts | Adds idempotent backfill script (dry-run by default) to classify legacy rows and populate new columns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- logIngestion derives stored kind from details.kind (single source of truth); drop the redundant kind param so it can't disagree with details - add OtherIngestionDetails variant so IngestionDetails covers every ingestion_kind enum value (+ buildIngestionLabel 'other' case) - normalise label (label ?? vendor) before DataTable so legacy rows stay searchable during the migration window - restore 'bulk upload' in the empty-state copy typecheck + lint clean; 404 tests pass.
|
Thanks — all four addressed in
typecheck + lint clean, 404 tests pass. Generated by Claude Code |
Why
ingestion_logwas built for invoices, then license-request ingestion (the "new request for a user" flow from Microsoft Forms) was squeezed into the same table by overloading invoice columns —invoice_numberheld a form-response GUID,linked_invoice_id(a real FK toinvoices.id) held alicense_requests.id, and the"filtered"outcome was reused to mean "deduped". The Settings → Ingestion table, the dashboard activity feed, and the settings copy all rendered invoice semantics, so a license request showed up as a half-empty, partly-wrong invoice row — and the overloaded FK was a latent bug (silent audit-insert rejection, or a cross-wired "Download document" link).This PR makes the two ingestion kinds first-class and distinguished end-to-end, and is built to make future ingestion types cheap to add. Design docs live in
specs/034-ingestion-types-distinction/(proposal.html,implementation-plan.html— Nothing-styled,implementation-notes.html).What changed
Schema (P0 — additive, migration
0023)ingestion_kind+ingestion_source_typeenums.ingestion_loggainskind(NOT NULL DEFAULT'invoice'),source_type,label,entity_type,entity_id,details(jsonb) + a kind index. Deprecated invoice columns retained (expand step).IngestionDetailsdiscriminated union insrc/types/ingestion.ts.Write path (P2)
logIngestion()is the canonical discriminated logger;logIngestionAttempt()kept as a dual-writing deprecated shim so the ~22 invoice call sites are untouched and legacy columns stay populated during the window.linked_invoice_idand"filtered"; dedup is nowsuccess+details.deduped. Closes the latent cross-type FK bug.Read path + UI (P3)
getIngestionHistoryreturnskind/details/entity.registry.tsx) + server-safe label builder (labels.ts)./requests/:id), and All / Invoices / License requests sub-tabs — using the in-repo Nothing design primitives.Backfill (P1)
scripts/backfill-ingestion-kind.ts— idempotent, dry-run by default, classifies legacy rows byinvoice_number ∈ license_requests.form_response_id.Open questions
All five from the proposal resolved with the plan defaults (sync stays separate; filter + sub-tabs; no new outcome enum;
user_importreserved; JSONB details).Test plan
pnpm typecheckclean ·pnpm lint0 warnings ·pnpm test404 pass (incl. 10 new unit tests for the label builder + registry).0023generated and reviewed (drizzle-migration-reviewer → PASS, purely additive).0023and runscripts/backfill-ingestion-kind.ts --applyagainst the Neon preview branch, then exercise invoice + license-request ingestion end-to-end. Planned against this PR's Vercel/Neon preview.Deferred
https://claude.ai/code/session_018kwPp9j8BHkKQwFSRfvvvR
Generated by Claude Code