Commit Graph

922 Commits

Author SHA1 Message Date
Marcel
f65296172f test(geschichte): document FK-load-bearing cleanup order (#805)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m28s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 4m57s
CI / fail2ban Regex (pull_request) Successful in 53s
CI / Semgrep Security Scan (pull_request) Successful in 28s
CI / Compose Bucket Idempotency (pull_request) Successful in 37s
Adds a comment on the @AfterEach deletion sequence explaining that journey_items
must be removed before their referenced documents and geschichten.

Addresses @sara review on PR #806.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:23:33 +02:00
Marcel
00baf0d881 refactor(geschichte): make flush-before-delete explicit on note-less cleanup (#805)
Adds flushAutomatically = true to @Modifying on deleteNoteLessByDocumentId so
the flush-before-bulk-delete contract is explicit instead of relying on
Hibernate AUTO flush-mode behaviour.

Addresses @markus review on PR #806.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:22:12 +02:00
Marcel
84c5ff5b3f docs(audit): correct DOCUMENT_DELETED payload javadoc to none (#805)
The call site passes a null payload and carries the id in the documentId
column (matching FILE_UPLOADED), so the javadoc claiming Payload:
{"documentId": "uuid"} misdescribed the audit schema. Audit javadocs are the
contract forensic queries are written against.

Addresses @felix, @nora and @elicit review on PR #806.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:21:31 +02:00
Marcel
e9b8920b3b refactor(document): import ApplicationEventPublisher instead of FQN field (#805)
Replaces the fully-qualified org.springframework.context.ApplicationEventPublisher
field declaration with an import + simple type name, consistent with every
other field in DocumentService.

Addresses @felix review on PR #806.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:20:49 +02:00
Marcel
c2b98e6e84 fix(document): record actorId on DOCUMENT_DELETED audit (#805)
deleteDocument now threads the acting user through to the audit log so
DOCUMENT_DELETED records who deleted the document, matching every other
audited write path (storeDocument, updateDocument, applyBulkEditToDocument,
attachFile). Tightens the AC-7 assertion from any() to the concrete actor.

Also adds the missing ApplicationEventPublisher @Mock to DocumentServiceTest,
which the publishEvent call (added with the cascade fix) left null.

Addresses @nora (CWE-778, blocker), @felix and @sara review on PR #806.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:17:52 +02:00
Marcel
44869d64f7 fix(geschichte): delete note-less journey items before document delete to prevent chk constraint 500 (#805)
Publishes DocumentDeletingEvent from DocumentService.deleteDocument before
deleteById; JourneyItemDocumentDeleteListener handles it synchronously so
note-less items are gone before ON DELETE SET NULL fires on note-carrying rows.
Plain @EventListener chosen over AFTER_COMMIT (fires too late) and @Async
(breaks rollback atomicity) — see ADR-038. Adds DOCUMENT_DELETED to AuditKind.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 17:54:54 +02:00
Marcel
7ca6492fc0 test(geschichte): rename journey items integration test — drop misleading end_to_end suffix (#795)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 6m9s
CI / OCR Service Tests (pull_request) Successful in 28s
CI / Backend Unit Tests (pull_request) Successful in 5m28s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:15:24 +02:00
Marcel
d0fc8ce995 feat(geschichte): allow journey items on STORY-type Geschichten (#795)
Delete the JOURNEY-only type guard in JourneyItemService.append() so the
existing item endpoints serve both Geschichte types. GeschichteType has
exactly two constants, so an allowlist replacement would be unreachable
dead code. Fix the not-found messages that claimed "Journey", and remove
the now-orphaned GESCHICHTE_TYPE_MISMATCH error code end to end
(ErrorCode, errors.ts union + mapping, i18n keys in de/en/es).

Tests: three STORY append unit tests written red against the guard, plus
end-to-end STORY coverage (append+retrieve, V72-style position-gap rows
incl. removal, dangling document-deleted item). The two STORY-rejection
tests die with the guard — no third enum value exists to feed it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:21:33 +02:00
Marcel
b9bed19610 feat(geschichte): bound title and JOURNEY intro at all three layers
Title: requireTitle() turns the VARCHAR(255) DB bound into a friendly 400
(GESCHICHTE_TITLE_TOO_LONG). JOURNEY intro: MAX_INTRO_LENGTH = 4000 in
bodyForType() plus a V75 CHECK as atomic backstop (defensive clamp first,
STORY bodies exempt) — GESCHICHTE_INTRO_TOO_LONG. Both codes wired through
ErrorCode.java, errors.ts, getErrorMessage and de/en/es. DB-layer boundary
pins added: exactly-2000 note insert (V74 CHECK) and 4000/4001 intro
inserts against real Postgres. Docs: error-code lists, db puml diagrams.

The matching maxlength attributes land with the component commits.

Review round 3: Markus (b), Nora (1), Sara (DB 2000 boundary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:28:08 +02:00
Marcel
98805f596a fix(db): make V74 self-healing on databases that ran the base branch
Dedup DELETE + note clamp before the DDL so the unique index and CHECK
cannot fail mid-migration (failed Flyway row -> backend boot loop) on a
DB that served writes under the old code (no dedup guard, 5000-char
notes). No-ops on a clean database.

Note: this changes V74's checksum — dev databases that already ran V74
on this branch need 'flyway repair' (or a fresh DB). CI and prod run
from V73 or clean and are unaffected.

Review round 3: Tobias (1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 07:51:20 +02:00
Marcel
2e9902e8a7 fix(journeyitem): translate only the dedup-index violation to 409
Any other DataIntegrityViolationException at flush (e.g. an FK violation
on a concurrently deleted document) is rethrown instead of being
mislabeled as JOURNEY_DOCUMENT_ALREADY_ADDED. Match on the
uq_journey_items_geschichte_document constraint name.

Review round 3: Markus (a), Felix suggestion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 07:49:31 +02:00
Marcel
a65a55448e test: align unit stubs and fixtures with the review-round changes
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m16s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 4m17s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
CI caught three spots the targeted local runs missed: the relevance-path
unit tests still stubbed findAllById (the path now calls findByIdIn — the
batchMetadata stubs legitimately keep findAllById), the second
GeschichtenCard test file still expected the removed email fallback, and
the AND/OR-toggle describe lacked the wait-for-slide-transition guard its
sibling describe documents — the flake that failed run 2208.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 08:37:31 +02:00
Marcel
d6ac88a211 docs(adr): ADR-036 — geschichte responses are views, never entities
Captures the read-model invariant that was only living in a code comment:
views are assembled in-transaction, entities never cross the controller
boundary in the geschichte domain. CLAUDE.md §DTOs now names the exception
so the convention text stops contradicting the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:29:21 +02:00
Marcel
d34afb2298 fix(geschichte): store JOURNEY intros as plain text — no HTML entity encoding
The OWASP sanitizer entity-encodes ('&' → '&amp;') while JourneyReader
renders the intro via Svelte text interpolation — a curator typing
'Müller & Söhne' saw 'Müller &amp; Söhne', re-encoded cumulatively on every
editor round-trip. JOURNEY bodies now bypass the sanitizer (safe: the reader
never uses {@html}); STORY bodies keep the full allow-list sanitization.
This makes the code match the PR's documented design note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:28:09 +02:00
Marcel
e077dba595 fix(geschichte): stop exposing author email in the list projection
GET /api/geschichten shipped every author's AppUser email to all readers via
GeschichteSummary.AuthorSummary — contradicting the documented rule that
author projections never expose email or group memberships. The frontend
only used it as a display-name fallback; it now falls back to [Unbekannt],
matching the server-side rule in GeschichteService.toView.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:25:11 +02:00
Marcel
b3d54a12c4 feat(journeyitem): V74 — partial unique index + note-length CHECK as atomic backstops
The append dedup guard was check-then-insert: two concurrent appends of the
same document both pass exists() and both insert. The partial unique index
on (geschichte_id, document_id) WHERE document_id IS NOT NULL closes the
race; append saveAndFlush-es and maps the DataIntegrityViolationException to
the same 409 JOURNEY_DOCUMENT_ALREADY_ADDED as the friendly pre-check. The
CHECK on note length pins the 2000-char contract in the schema, mirroring
chk_text_length on transcription_blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:21:41 +02:00
Marcel
02efe738a0 fix(geschichte): make the documented error-code contract real
GESCHICHTE_TYPE_IMMUTABLE and JOURNEY_NOTE_TOO_LONG were declared in
errors.ts, translated, and documented — but never existed in the backend.
update() now rejects a type change with 409 (omitted/same type still pass);
note length is enforced at 2000 with its own code, matching the frontend
maxlength and the i18n message (resolves the #793 discrepancy in favour of
the spec). JOURNEY_ITEM_NOT_IN_JOURNEY is deleted everywhere instead — the
deliberate 404 posture for cross-journey item ids must not leak existence
via a distinct code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:14:35 +02:00
Marcel
ae0b143685 fix(journeyitem): explicit JPQL for the document-dedup guard — derived query unmappable
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 3m30s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Failing after 3m46s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m11s
POST /api/geschichten/{id}/items with a documentId failed 500: Spring Data
resolved the derived existsByGeschichteIdAndDocumentId path as a direct
documentId attribute (shadowed by the transient getDocumentId() getter)
instead of document.id, producing JPQL Hibernate cannot map. Existing tests
only appended note items, so the document branch was never exercised.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:05:27 +02:00
Marcel
282c819e2a feat(geschichte): carry updatedAt in GeschichteSummary projection
The reader dashboard renders 'bearbeitet vor X' from updatedAt, but the
summary projection dropped the field when the list endpoint moved off the
entity — drafts have no publishedAt to fall back on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:01:09 +02:00
Marcel
e988c3eae7 fix(geschichte): return GeschichteView from create/update — kill write-path 500
PATCH /api/geschichten/{id} (save draft, publish) returned the raw entity;
with open-in-view false, Jackson serialized the lazy items collection after
the transaction closed and every save failed with LazyInitializationException.
Write methods now assemble GeschichteView in-transaction, completing the
read-model boundary already used by GET — entities no longer cross the
controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:06:18 +02:00
Marcel
522d1f3ec9 fix(search): load relevance-path documents with Document.list entity graph
The pure-text RELEVANCE fast path loaded documents via plain findAllById,
which carries no entity graph. With Document.tags LAZY (ADR-022) and no
surrounding transaction, resolveDocumentTagColors hit the dead proxy and
every q-only search (document picker typeaheads) failed with 500
LazyInitializationException. Dedicated findByIdIn declares the same fetch
shape as the other search loaders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 22:58:13 +02:00
Marcel
44f15dd4a2 fix(geschichte): persist GeschichteType on create — JOURNEY was silently dropped
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m25s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m56s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
GeschichteUpdateDTO lacked a `type` field, so the `type: 'JOURNEY'` sent by
JourneyCreate was discarded by Jackson and every new Geschichte was saved as
STORY. The edit page branched on type, so journeys always showed the STORY
editor with no document-adding capability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:02:58 +02:00
Marcel
8adc39e2ce test(geschichte): add integration tests for documentId JPQL EXISTS filter
Verifies the EXISTS subquery in findSummaries actually filters by document
membership — a journey containing the document appears, one without does not.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:51:51 +02:00
Marcel
949421a076 fix(geschichten): restore documentId filter via journey_items EXISTS subquery
The PR removed the documentId filter from list() along with the old
Geschichte.documents ManyToMany, but the document-detail page and its
frontend server still query GET /api/geschichten?documentId=<id> to show
related stories. Without the filter the endpoint silently returned every
published story. Restores the filter through a JPQL EXISTS check on
journey_items so only journeys that include the given document are returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:18:28 +02:00
Marcel
353945c952 fix(journey-item): enforce duplicate-document guard in append()
Adds JOURNEY_DOCUMENT_ALREADY_ADDED to ErrorCode, an
existsByGeschichteIdAndDocumentId() repo method, and a 409 guard in
JourneyItemService.append() — the error code was registered on the
frontend but never thrown on the backend, allowing concurrent tabs to
add the same document twice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:18:00 +02:00
Marcel
77cbbd34a0 test(journeyitem): verify findSummaryByIdInternal never called before JOURNEY-type guard
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m46s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:38:25 +02:00
Marcel
84b47f1836 fix(geschichte): move DocumentSummary to journeyitem sub-package
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:37:07 +02:00
Marcel
99111273e5 refactor(document): rename getSummaryById to findSummaryByIdInternal to signal scope-check bypass
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m52s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m9s
The method intentionally skips permission checks and tag-colour resolution.
Renaming it to findSummaryByIdInternal makes the internal-only contract
visible at every call site, closing the latent CWE-284 risk flagged in
the PR review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:13:35 +02:00
Marcel
f09c79744e fix(geschichte): restore getView() on GeschichteService with @Transactional(readOnly=true) — fixes two-call transaction gap
Re-inject JourneyItemService into GeschichteService (no cycle:
JourneyItemService → GeschichteQueryService, not GeschichteService).
Add getView(UUID) that loads the Geschichte and its items in a single
@Transactional(readOnly=true) session. Controller now delegates to
getView() instead of making two separate service calls. Tests updated
to stub getView() and cover the new method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:12:25 +02:00
Marcel
1108277472 refactor(geschichte): extract PersonNameFormatter to eliminate duplicated name-join logic
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m15s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m51s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
Create PersonNameFormatter with a single static join(firstName, lastName) method.
Replace the inline string concatenation in GeschichteService.toView() and the
private join() method in JourneyItemService with calls to PersonNameFormatter.join().
The new helper handles null-safety and trimming consistently in one place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:53:49 +02:00
Marcel
9db3b41fdb docs(api): document reorder full-list contract in OpenAPI
Add @Operation annotation to reorderItems() clarifying that itemIds must
contain ALL item IDs for the journey in the desired order — a partial list
returns 400 Bad Request. This surfaces the contract in the generated
OpenAPI spec and Swagger UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:52:02 +02:00
Marcel
73004ce49f docs(document): document scope-check bypass on getSummaryById
Clarify in the Javadoc that getSummaryById intentionally skips scope checks
and tag-colour resolution. This is safe under the current single-tenant model
and is explicitly used by JourneyItemService.append() to validate that a linked
document exists before persisting a JourneyItem.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:51:10 +02:00
Marcel
c31f82a69c fix(test): use nullValue() matcher instead of doesNotExist() for null note field
doesNotExist() asserts the key is absent from the JSON object, but Jackson
serializes a null Optional<String> as {"note": null} — the key is present with
a null value. nullValue() correctly matches that case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:50:17 +02:00
Marcel
f9ae6a91ba test(journeyitem): add integration tests for append and reorder against real PostgreSQL
Add two service-level integration tests to JourneyItemIntegrationTest:
- append_persists_item_at_position_10: verifies that the first append to an
  empty journey creates an item at position 10 in the DB.
- reorder_swaps_positions_atomically: appends two items then reorders them,
  asserting the DB reflects the new position assignment.
Both tests use the SecurityContextHolder authentication pattern from
GeschichteServiceIntegrationTest and mock S3Client to avoid MinIO connections.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:49:23 +02:00
Marcel
ad90ae75bf fix(journeyitem): use JOIN FETCH to eliminate N+1 document queries
Add findByGeschichteIdWithDocument() to JourneyItemRepository with a
LEFT JOIN FETCH on document. getItems() now uses this query so that all
documents for a journey's items are loaded in a single SQL round-trip.
toView() now reads item.getDocument() directly from the already-fetched
association instead of issuing a separate documentService.getSummaryById()
call per item.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:46:54 +02:00
Marcel
164178ecf1 refactor(geschichte): assemble GeschichteView in controller — break GeschichteService↔JourneyItemService cycle
GeschichteService.getById() now returns the Geschichte entity (with the
DRAFT visibility guard intact). The controller calls journeyItemService.getItems()
and geschichteService.toView() to assemble the GeschichteView, removing the
need for GeschichteService to hold a reference to JourneyItemService.
Tests updated accordingly: GeschichteServiceTest tests toView() directly;
GeschichteControllerTest stubs both service calls; integration test uses the
two-step pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:45:09 +02:00
Marcel
3f36d2a7f1 chore(test): remove JacksonConfig from GeschichteControllerTest @Import
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m47s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
JacksonConfig was deleted (empty placeholder) — remove the now-stale
import and @Import reference from the controller slice test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:07:03 +02:00
Marcel
3d80bc656c refactor(journeyitem): use saveAll in reorder for efficiency
Replace the per-item save() loop in reorder() with a single
saveAll() call, reducing database round-trips for large journeys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:05:28 +02:00
Marcel
4a0fed617a refactor(geschichte): route all reads through GeschichteQueryService
JourneyItemService no longer injects GeschichteRepository directly.
GeschichteQueryService gains findById() so JourneyItemService.append()
can load the Geschichte entity via the service layer, satisfying the
cross-domain layering rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:04:21 +02:00
Marcel
7ba6342a84 chore(config): remove empty JacksonConfig placeholder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:02:51 +02:00
Marcel
598ad622e7 fix(journeyitem): use specific error codes in append() — JOURNEY_AT_CAPACITY and GESCHICHTE_TYPE_MISMATCH
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m21s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m56s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
- JourneyItemService.append(): replace VALIDATION_ERROR with GESCHICHTE_TYPE_MISMATCH (409 conflict)
  for non-JOURNEY type guard and JOURNEY_AT_CAPACITY (409 conflict) for 100-item cap
- JourneyItemServiceTest: update assertions to expect the new specific error codes
- CLAUDE.md: expand geschichte/ package table entry with GeschichteQueryService and journeyitem/ sub-domain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:31:30 +02:00
Marcel
c5611250ec test(journey): rename updateItemNote test to clarify Optional deserialization semantics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:17:25 +02:00
Marcel
e400b1d77e feat(error): add GESCHICHTE_TYPE_MISMATCH error code with i18n (de/en/es)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:15:45 +02:00
Marcel
1fb0c41216 feat(error): add JOURNEY_AT_CAPACITY error code with i18n (de/en/es)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:13:47 +02:00
Marcel
147aa56386 feat(audit): add JOURNEY_ITEM_NOTE_UPDATED audit kind and wire into updateNote()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:04:43 +02:00
Marcel
7c06609816 refactor(journey): make toView() and toSummary() package-private
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:03:24 +02:00
Marcel
2ae1bb3a30 fix(journey): reorder() throws 404 when Geschichte does not exist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:02:03 +02:00
Marcel
69db198319 refactor(geschichte): introduce GeschichteQueryService with existsById()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:00:32 +02:00
Marcel
e157d90b53 docs(backend): add JourneyItemService and GeschichteQueryService to CLAUDE.md package table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:58:20 +02:00
Marcel
7ed0032661 fix(review): replace Set<Person> with Set<PersonView> in GeschichteView — prevents leaking admin fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:12:13 +02:00