feat(export): export approved requests exactly once to the ERP mock - #36
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ion access control Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…LS with integration proofs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…for #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…boss queues Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…nd download routes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…-only audit, composite FK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
- sign-up requires the invitation id (link) plus the invited e-mail – no takeover by address alone - one company per user (unique index), deterministic membership lookup, actor from membership - organization plugin accepts only admin/clerk roles - configurable client-IP source for the auth rate limit; local secret refused in production - invite page: zod input, 404 for clerks, shows the invitation link; signup needs the link - tests: wrong/missing invitation id, foreign set-active/list-members, last admin, roles - docs: operations (rate limit/proxy, recovery), data model, exceptions register (admin plugin) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…e rejection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
… docs for #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…secret The image sets NODE_ENV=production, so the previous check blocked the local compose stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…re script Python 3.13 package requestflow_ai (src layout), docling/fastapi/google-genai pinned, CPU-only torch via the PyTorch CPU index, dev tools ruff/pyright/pytest. The synthetic PDF fixture is generated by scripts/make_fixtures.py (reportlab). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Test-first per ADR-0001 D8: quote normalisation (whitespace, case, NFKC, hyphenation), German number and date formats, and the verifier rules that turn unsupported model claims into unverified. Also adds the segment and model-output types the tests build on; the verifier does not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
normalize_text folds NFKC, soft hyphens, line-break hyphenation, typographic dashes/quotes, whitespace and case. values parses German/ISO numbers and DD.MM.YYYY, D.M.YY and ISO dates. verify_field only keeps or downgrades the model's status: a quote not in the cited segment, an unknown segment, a value inconsistent with the quote, found without evidence or value, and missing with a value all become unverified with a reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ction (red) EML: body lines with 1-based line locators, From/Subject header segments, RFC 2047 and quoted-printable decoding, HTML-only bodies, header newline collapse, no attachment payloads. PDF: textline segments with page + top-left bbox via docling-parse (model-free); the layout-pipeline test only runs with AI_TEST_DOCLING_MODELS=1. Detection by magic bytes; .msg rejected for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
EML via the standard library email package (policy=default): From/Subject header segments and one segment per non-empty body line, HTML-only bodies reduced to text lines. docling's EMAIL backend was checked but emits paragraphs without provenance, so it cannot give line locators. PDF via docling: the default textlines pipeline reads docling-parse text lines (page + top-left bbox, no ML models, no network); the opt-in layout pipeline uses DocumentConverter (OCR and tables off) and the heron layout model. docling is imported lazily. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…ex model client (red) The model client is exercised through the real google-genai SDK with an httpx MockTransport replaying recorded generateContent bodies: eu multi-region URL, bearer auth, structured-output config, token usage, fail-closed init (no project, no credentials, dev flag without key), no silent switch to API key mode, and schema-invalid output. Adds the env-based Settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…lient Prompt extract_header_v1.md (system instruction) plus render_document, which puts segment-id-prefixed lines between <document> delimiters and neutralises delimiter-like tags inside the document. GeminiModelClient calls Vertex via google-genai with response_schema = ModelExtraction, JSON mime type, temperature 0 and no tools; schema-invalid output raises ModelOutputError. build_model_client needs VERTEX_PROJECT and credentials, loads ADC eagerly, refuses API-key mode for Vertex, and allows the Gemini API only with AI_ALLOW_GEMINI_API_DEV=true plus GEMINI_API_KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Pipeline: PDF and EML end to end with recorded model responses, a model date contradicting its own quote, the prompt-injection mail (the recorded model obeys and invents a quote -> unverified), and the no-text PDF that skips the model. API: bearer auth (401 + WWW-Authenticate), response shape, request id handling, 400/413/415/422/429/502 mapping without echoing input, JSON logs with IDs only. Contract: the committed OpenAPI file equals the app's schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…, approve/reject actions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1
…es; refuse overlong reasons; cross-tenant correction tests
7 tasks done
…T adapter with timeout
…ide local), bounded mock store
…port stage; integration tests
…ody bounds; contract 411/413
…E smoke ends at Exportiert; compose + env wiring
…adable body retryable, timing-safe mock token, faults and timeout checked at start, skip reasons logged; docs
Owner
Author
Frischer Review (unabhängiger Subagent, read-only,
|
| # | Schwere | Befund | Auflösung |
|---|---|---|---|
| S1 | should-fix | ERP-Längenlimits erst beim Export geprüft → zu langer, unkorrigierter KI-Wert bleibt dauerhaft in ERROR (nach Freigabe keine Korrektur mehr) | Freigabe wird abgelehnt (value_too_long), solange ein Wert die Limits verletzt; Clerk korrigiert zuerst. Integrationstest „refuses approval while a value is too long…". Betreff war schon begrenzt (Mail 300, Dateiname 200) – Test ergänzt |
| S2 | should-fix | Fehler beim Lesen des Antwort-Bodys wurde zu permanentem contract_violation |
Lesefehler (Reset/Timeout) → unreachable/timeout, retryable; nur gelesener, vertragswidriger Body ist permanent. Unit-Test |
| S3 | should-fix | Token-Vergleich im Mock nicht timing-safe | timingSafeEqual über SHA-256-Digests |
| S4 | should-fix | ERP_MOCK_FAULTS erst beim ersten POST geprüft |
Prüfung in loadConfig (Start schlägt fehl), Test |
| S5 | should-fix | ERP_TIMEOUT_MS unbegrenzt, Zeilensperre + Pool-Verbindung während des Aufrufs |
Maximum 20 s (unter statement_timeout 30 s, weit unter Job-Expiry 15 min), Test; in operations.md begründet |
| N1 | nit | „skipped" still | export.skipped mit Grundcode (nur IDs) |
| N2 | nit | Doku „5xx retried" ungenau | exakte Codes (408, 429, 500, 502, 503, 504) |
| N3 | nit | 409-Zeile empfahl Reprocess | Konflikt zuerst auf ERP-Seite lösen – Reprocess sendet denselben Body |
| N4 | nit | Mock-Route fehlte in der Architekturkarte | ergänzt |
| N5 | nit | .env.example ERP_BASE_URL bricht Compose, wenn als .env kopiert |
Kommentar ergänzt (Compose setzt http://web:3000/api/erp-mock) |
| N6 | nit | Folge des In-Memory-Speichers bei Neustart nicht benannt | in api.md/operations.md und der decision-needed ergänzt |
Generated by Claude Code
This was referenced Sep 23, 2026
Fluory
marked this pull request as ready for review
September 23, 2026 07:29
This was referenced Sep 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warum
Fixes #9 · Teil von Epic #2 · gestapelt auf #35 (Basis
claude/feat-review-ui-8).Arbeitsstand
contracts/erp-export.openapi.yaml) mit Idempotency-Key an den ERP-Mock und landen genau einmal im Status EXPORTED.request_exports), REST-Adapter, Mock + Route hinter Flag, Export-Handler +drainExports()im Worker, Reprocess für Stufe export, Seite zeigt ERP-Referenz/Wiederholungen, Tests (Unit, Integration, E2E bis „Exportiert"), Doku, frischer Review inkl. Fixes.Was ist passiert (Klartext)
Freigegebene Anfragen werden jetzt automatisch an das (simulierte) ERP geschickt – und zwar garantiert nur einmal. Dafür sorgen drei Dinge zusammen: Jede Anfrage trägt ihren festen Schlüssel, an dem das ERP eine Wiederholung erkennt und dieselbe Referenz zurückgibt; auf unserer Seite gibt es genau einen Export-Eintrag pro Anfrage; und während des Exports ist die Anfrage gesperrt, sodass zwei Durchläufe nicht gleichzeitig exportieren. Getestet ist das mit simulierten Ausfällen (Fehler 503, Zeitüberschreitung, „Antwort verloren") und doppelt zugestellten Aufträgen: Am Ende steht immer genau ein Datensatz im ERP. Scheitert der Export dauerhaft, steht die Anfrage sichtbar auf „Fehler (Export)" und kann erneut angestoßen werden. Die Prüfseite zeigt die ERP-Referenz. Der ERP-Simulator ist nur mit
ERP_MOCK_ENABLED=trueerreichbar. Freigeben geht nicht mehr, solange ein Wert zu lang für das ERP ist – dann erst korrigieren. Nebenbei behoben (#7): ein Auftrag mit unbrauchbaren IDs bringt den Worker-Durchlauf nicht mehr aus dem Tritt. Compose-Änderung: Worker und Web bekommen die ERP-Variablen (lokale Standardwerte).Plan-Pflicht (SYSTEM.md §4)
Impact Manifest
export(ERP-Port, REST-Adapter mit Timeout, Export-Handler,drainExports),erp-mock(idempotenter Empfänger mit Fehlerinjektion, Route hinterERP_MOCK_ENABLED),jobs(Reprocess Stufe export;drain()-Härtung),requests(APPROVED → EXPORTED / ERROR(export)),review(currentFieldValues, Freigabe prüft ERP-Limits, Exportstatus in der Ansicht),db(Migrationen 0011/0012),config(ERP_*), Worker, compose, E2E-Smoke.POST /v1/quote-requestsmitIdempotency-Key: <requestId>(201/200/400/401/409/411/413/503); RoutePOST /api/erp-mock/v1/quote-requests(404 ohne Flag, Bearer-Token, ≤ 64 KiB); Tabelleapp.request_exports(uniquerequest_id, Key = Request-ID per Check, FORCE RLS, Composite-FK, kein DELETE); EnvERP_BASE_URL,ERP_TOKEN,ERP_TIMEOUT_MS(≤ 20 s),ERP_MOCK_ENABLED,ERP_MOCK_FAULTS.statement_timeoutdes Pools 30 s, Job-Expiry Export 15 min.Geändert
contracts/erp-export.openapi.yaml,src/features/export/{contract,erp-client,payload,repository,export-job,index}.ts+ generierteserp-export.contract.tssrc/features/erp-mock/{mock,index}.ts,src/app/api/erp-mock/v1/quote-requests/route.tssrc/db/schema/app.ts, Migrationen0011_export.sql,0012_export_force_rls.sqlsrc/features/jobs/{drain,reprocess}.ts,src/worker.ts,src/config/env.tssrc/features/review/{review,index}.ts,src/app/requests/[id]/{page.tsx,messages.ts}src/features/{export,erp-mock}/*.test.ts,src/config/env.test.ts,tests/architecture/contract-types.test.ts,tests/integration/{export,erp-mock-route,processing,review,upload-routes,intake}.test.ts,tests/e2e/review-smoke.spec.ts,playwright.config.tscompose.yaml,.env.example,package.json(contract:types)docs/technical/{api,operations,data-model,architecture}.md,CHANGELOG.mdNachweis (SYSTEM.md §11)
verify:changed: typecheck + lint + betroffene Unit-/Integrationstests grünverify: lokal grün (Exit 0, Node 24) gegen echtes PostgreSQL 17 + SeaweedFS – Unit 121/121, Integration 81/81, depcruise ohne Verstöße, Build, Audit (1 moderate, Schwelle high); CIcheck: siehe Checks dieses PRsverify:full/ E2E-Spec:tests/e2e/review-smoke.spec.tslokal grün bis „Exportiert" mit ERP-Referenz (Worker, gebaute App, KI-Stub, Mock); CI per Labelverify-fullunique(request_id)+ APPROVED → EXPORTED unter Zeilensperre → Migration 0011/0012, Integration „duplicate delivery…" (Mutationsprobe: ohne Sperre scheitert der Test); injizierte 5xx/Timeouts + doppelte Zustellung → genau einmal EXPORTED → Integration „survives a 503, a timeout and a lost response…", „duplicate delivery…", „an ERP that stored the request before our commit…"; Timeout konfiguriert + Fehler sichtbar als ERROR (Stufe export) → Unit Adapter, Config-Test, Integration „permanent refusal…", „retries exhausted…"; Mock nur mitERP_MOCK_ENABLED=true→ Route-Test 404 (Flag aus) + Flag-an-Testsreview-prinkl. Security/API/Migrations-Regeln – 0 Blocker, 5 should-fix + 6 nits, alle behoben (PR-Kommentar)Doku-Entscheidung (genau eine)
docs/technical/data-model.md,docs/technical/api.md,docs/technical/operations.mddocs/technical/architecture.md[Unreleased](sichtbares Feature oder Verhalten – im selben PR, nie „später")Entferntes oder Umbenanntes: nichts entfernt
Dateigrößen und neue Bausteine (SYSTEM.md §7)
Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):
Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen
Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:
extraction/ai-client.ts(Timeout, Fehlerklassen, zod) undjobs/drain.ts– ERP-Adapter unddrainExportsfolgen denselben Mustern;export/erp-mockwaren Skelette aus chore(app): TS app skeleton, docker compose and verify commands #3Subagent-Einsätze
Risiken / offene Punkte
🤖 Generated with Claude Code
https://claude.ai/code/session_01DJ5vaKvTYiMvdngT4d3xo1