bug: deleting a document linked via a note-less journey_item 500s at DB constraint #805

Open
opened 2026-06-11 13:16:29 +02:00 by marcel · 1 comment
Owner

Summary

Deleting a Document that is linked via a JourneyItem with document_id IS NOT NULL and note IS NULL results in a database-level 500 (ERROR: new row for relation "journey_items" violates check constraint "chk_journey_item_not_empty").

Definition — "note-less": throughout this issue, a journey item is note-less iff note IS NULL OR note = ''. A note-carrying item has non-empty text. The cascade and every AC use this exact meaning — never read "note-less" as "NULL only."

Root cause

Two constraints in V72 encode contradictory rules for note-less document items:

  • FK journey_items.document_id → documents.id ON DELETE SET NULL — when the linked document is deleted, Postgres nulls out document_id.
  • chk_journey_item_not_empty CHECK constraint — requires at least one of document_id or note to be non-null.

A note-less item (document_id set, note IS NULL) satisfies chk_journey_item_not_empty while the document exists. When the document is deleted, Postgres tries to SET NULL the FK, which would leave both columns null — a direct violation of the CHECK. Postgres aborts the transaction with a constraint error. This fires at flush/commit, after the controller returns, so GlobalExceptionHandler does not map it to a clean 400 like a normal DataIntegrityViolationException (handleDataIntegrityViolation logs only the constraint name at WARN, CWE-209-safe) — it falls through to handleGenericSentry.captureException → HTTP 500. So every occurrence today is also a real Sentry/GlitchTip event, not just a bad status code.

Scope

This was pre-existing before #795 (PR #804), but that PR widens the blast radius: until now only JOURNEY curators could create note-less items; after #795, every STORY editor using StoryDocumentPanel creates note-less items by default (adding a document without a curator note). The existing integration test JourneyItemIntegrationTest.deleting_document_sets_item_document_to_null_not_delete_item (line 205) already documents the note-carrying workaround: its item carries a note precisely to avoid triggering this constraint.

Reproduction

  1. Create a STORY or JOURNEY Geschichte.
  2. Add a document item without a note (via StoryDocumentPanel or JourneyEditor).
  3. Delete the linked document from the archive.
  4. Observe: HTTP 500 from Postgres at the journey_items FK set-null trigger.

Resolved approach (converged after three rounds of multi-persona review)

Decision (UX): when a document is deleted, note-less journey items referencing it silently vanish (the row is deleted). No warning dialog, no RESTRICT, no curator notification. Note-carrying items keep their existing placeholder behavior.

Decision (audit): no per-item audit. The existing document-delete audit entry is the sole record. The event-driven cleanup deliberately bypasses JourneyItemService.delete (which would emit AuditKind.JOURNEY_ITEM_REMOVED, JourneyItemService.java:158); the listener calls the repository directly and writes no journey-item audit rows. Rationale: a multi-journey document delete would otherwise produce N audit rows for a single user action; the document-delete entry is sufficient provenance.

Mechanism — event-driven, NOT a direct service call. JourneyItemService already injects DocumentService (JourneyItemService.java:9,39); making DocumentService call JourneyItemService back creates a constructor-injection cycle, which Spring Boot 4 / Spring Framework 7 prohibits (the app won't boot). Instead:

  • DocumentService.deleteDocument publishes a domain event (DocumentDeletingEvent, payload = documentId UUID only, no entity references across the boundary) via ApplicationEventPublisher (one-line @RequiredArgsConstructor injection) before documentRepository.deleteById(id) on DocumentService.java:1104, inside the same @Transactional boundary.
  • A small @Component JourneyItemDocumentDeleteListener in the geschichte.journeyitem package carries a synchronous @EventListener that calls the repository delete directly (no JourneyItemService method — routing through the service would re-introduce the audit emission the audit decision explicitly bypasses). Keep it a thin delegator. Use a plain @EventListener — it runs in the publisher's thread and transaction. Do NOT use @Async or @TransactionalEventListener(AFTER_COMMIT): AFTER_COMMIT fires after the FK SET NULL has already 500'd, and async runs outside the delete transaction (breaks AC-5). The listener-phase choice is load-bearing — comment it.
  • After the listener runs, deleteById proceeds; the FK ON DELETE SET NULL then nulls the remaining note-carrying rows (preserving the placeholder UX).

⚠️ This is the first custom domain event in the codebase. No existing precedent: the only @EventListener today is OcrTrainingService.java:235 on Spring's framework ApplicationReadyEvent lifecycle hook; OcrStreamEvent is a sealed interface streamed to an SSE Consumer (not a Spring ApplicationEvent), and there are zero ApplicationEventPublisher usages in main/. Establishing a publish/subscribe pattern for cross-domain cleanup is a precedent-setting decision: write an ADR in docs/adr/ (mirror the ADR-036 structure: context → decision → rejected alternatives → consequences). The ADR's central content is the load-bearing listener-phase decision (plain @EventListener, explicitly NOT AFTER_COMMIT, NOT @Async), plus the Spring-7 cycle constraint, the documentId-only payload boundary, the "documents must not depend on journey" direction, and the rejected alternatives (DB trigger / RESTRICT / relaxing the CHECK).

Do NOT relax chk_journey_item_not_empty and do NOT add a DB trigger — the CHECK is a real invariant and trigger logic is invisible to Java devs. No schema change → no new migration and no db-orm.puml / db-relationships.puml update. The only doc deliverable is the ADR.

Repository — explicit @Modifying @Query JPQL (a derived deleteByDocumentIdAndNoteIsNull breaks like existsByGeschichteIdAndDocumentId did, because the transient getDocumentId() getter (JourneyItem.java:48) makes Spring Data resolve documentId as an unmappable attribute — trap documented at JourneyItemRepository.java:33-44):

@Modifying(clearAutomatically = true)
@Query("DELETE FROM JourneyItem i WHERE i.document.id = :documentId AND (i.note IS NULL OR i.note = '')")
int deleteNoteLessByDocumentId(@Param("documentId") UUID documentId);

Use i.document.id (the real association path), never i.documentId. Return int (rows deleted) — useful for a WARN log in the listener and asserted in the idempotency test. clearAutomatically = true is required so AC-2's "note-carrying survives" assertion doesn't read a stale first-level-cache entity.

Acceptance criteria

  • AC-1 Given a note-less journey item whose document is deleted, the item is removed and the document delete succeeds (HTTP 2xx, no 500).
  • AC-2 Given a note-carrying item whose document is deleted, the item is retained with document_id = NULL and its note unchanged (existing placeholder UX preserved).
  • AC-3 Given a note-only item (never had a document), deleting any document does not affect it — item still exists AND note unchanged.
  • AC-4 Given a document used in multiple journeys, AC-1/AC-2 apply independently per referencing item (the asymmetric case: journey A note-less → gone, journey B note-carrying → survives as placeholder).
  • AC-5 All cleanup occurs within the document-delete transaction; if the delete fails, no journey items are removed (the listener's JPQL delete and the document delete roll back as one unit).
  • AC-6 A note-less item with an empty-string note (note = '' — satisfies the CHECK because NOT NULL, but dodges the service-layer normalizeNote) must not re-trigger the 500; it is cascaded. A whitespace-only note (note = ' ') is note-carrying and is preserved (the predicate is = '', not TRIM(note) = '' — pin this boundary so nobody widens it).
  • AC-7 The curator receives no error and no notification when their journey silently loses a note-less item. Assert both halves: the document-delete audit entry exists and is unchanged, AND no journey-item audit row is written. (Asserting only the absence would still pass if a regression dropped all audit.)

Tests (start red, real Postgres / Testcontainers only — the constraint never fires under H2)

  • ⚠️ All new tests must delete through DocumentService.deleteDocument(id), NOT documentRepository.deleteById(id). The existing journey integration tests (JourneyItemIntegrationTest.java:216) delete via the repository, bypassing the service where the event is published — copying that pattern makes the listener never fire and the test pass for the wrong reason (the single biggest green-but-wrong risk here). Autowire DocumentService.
  • AC-2 regression guard: update deleting_document_sets_item_document_to_null_not_delete_item to route through DocumentService (or add a sibling that does) — the repo-path version stays green even if the service path breaks the placeholder behavior.
  • deleting_document_linked_via_note_less_item_deletes_item_not_500 — headline (AC-1); confirm it fails with the constraint error before writing the listener.
  • AC-3 positive: note-only item untouched after an unrelated document delete.
  • AC-4 asymmetric: same document in two journeys (one note-less, one note-carrying) → first gone, second survives as placeholder.
  • AC-6: insert note = '' via raw JDBC / JdbcTemplate (the service can't produce it) → assert cascaded (no 500); insert note = ' ' via raw JDBC → assert preserved.
  • AC-5 rollback: @SpyBean/@MockBean the DocumentRepository, let the real listener run on real Postgres, then stub deleteById to throw a RuntimeException (Spring @Transactional rolls back on RuntimeException by default) after the event fires; re-query from Postgres after em.clear() and assert the note-less items are still present (guards against stale L1 cache hiding a non-rollback).
  • Idempotency / no-collateral: deleting a document in zero journeys returns 0 from deleteNoteLessByDocumentId, does not error, and leaves all unrelated journey-item counts unchanged (guards an over-broad JPQL DELETE missing the document.id predicate — assert the count, not just absence of an exception).

Sequencing & post-deploy

This P1 is a hard predecessor of #795 — merge #805 first, otherwise the first document delete after #795 ships hits the 500. Enforce the merge order in the PR description and milestone. Post-deploy verification:

  • Before deploy, capture the baseline: GlitchTip INTERNAL_ERROR events on the document-delete path, and/or count_over_time({...} |= "chk_journey_item_not_empty" [7d]) in Loki.
  • After deploy, actively trigger a document delete that would have 500'd pre-fix (smoke action) so "zero errors" reflects a path actually exercised, not a quiet observation window. The Loki signal goes silent because the violation no longer fires — there is no new log line to add; the absence of the existing WARN is the signal.

Hand-off to #795 (the deleted-document placeholder renderer ships there, not here)

#805 ships no UI. Seed these as real acceptance criteria in #795 before #805 closes (otherwise they disappear with this issue):

  • XSS: the placeholder note renders via Svelte {note} (auto-escaped), never {@html}JourneyItem.note is stored verbatim (CWE-79 tripwire, JourneyItem.java:41). Verify with a test using a <script>-bearing note. (Bonus: {note} also renders literal &/</> in real curator prose correctly.)
  • Accessibility (60+ audience): the "deleted document" placeholder row needs a real text label (not italic/color alone — not an accessible status cue), an aria-label on the remove control, and a ≥44px touch target on that control.

Notes

  • The "deleted-document placeholder" UX (document_id IS NULL items rendered as italic removable rows) depends on ON DELETE SET NULL working for items that do have a note. The fix preserves this for note-carrying items.
## Summary Deleting a `Document` that is linked via a `JourneyItem` with `document_id IS NOT NULL` and `note IS NULL` results in a database-level 500 (`ERROR: new row for relation "journey_items" violates check constraint "chk_journey_item_not_empty"`). > **Definition — "note-less":** throughout this issue, a journey item is *note-less* iff `note IS NULL OR note = ''`. A note-*carrying* item has non-empty text. The cascade and every AC use this exact meaning — never read "note-less" as "NULL only." ## Root cause Two constraints in V72 encode contradictory rules for note-less document items: - **`FK journey_items.document_id → documents.id ON DELETE SET NULL`** — when the linked document is deleted, Postgres nulls out `document_id`. - **`chk_journey_item_not_empty` CHECK constraint** — requires at least one of `document_id` or `note` to be non-null. A note-less item (`document_id` set, `note` IS NULL) satisfies `chk_journey_item_not_empty` while the document exists. When the document is deleted, Postgres tries to `SET NULL` the FK, which would leave both columns null — a direct violation of the CHECK. Postgres aborts the transaction with a constraint error. This fires at **flush/commit, after the controller returns**, so `GlobalExceptionHandler` does not map it to a clean 400 like a normal `DataIntegrityViolationException` (`handleDataIntegrityViolation` logs only the constraint name at WARN, CWE-209-safe) — it falls through to `handleGeneric` → `Sentry.captureException` → HTTP 500. So every occurrence today is also a real Sentry/GlitchTip event, not just a bad status code. ## Scope This was pre-existing before #795 (PR #804), but that PR **widens the blast radius**: until now only JOURNEY curators could create note-less items; after #795, every STORY editor using `StoryDocumentPanel` creates note-less items by default (adding a document without a curator note). The existing integration test `JourneyItemIntegrationTest.deleting_document_sets_item_document_to_null_not_delete_item` (line 205) already documents the note-carrying workaround: its item carries a note precisely to avoid triggering this constraint. ## Reproduction 1. Create a STORY or JOURNEY Geschichte. 2. Add a document item **without** a note (via `StoryDocumentPanel` or `JourneyEditor`). 3. Delete the linked document from the archive. 4. Observe: HTTP 500 from Postgres at the `journey_items` FK set-null trigger. --- ## ✅ Resolved approach (converged after three rounds of multi-persona review) **Decision (UX):** when a document is deleted, note-less journey items referencing it **silently vanish** (the row is deleted). No warning dialog, no RESTRICT, no curator notification. Note-*carrying* items keep their existing placeholder behavior. **Decision (audit):** **no per-item audit.** The existing document-delete audit entry is the sole record. The event-driven cleanup deliberately bypasses `JourneyItemService.delete` (which would emit `AuditKind.JOURNEY_ITEM_REMOVED`, `JourneyItemService.java:158`); the listener calls the **repository directly** and writes **no** journey-item audit rows. Rationale: a multi-journey document delete would otherwise produce N audit rows for a single user action; the document-delete entry is sufficient provenance. **Mechanism — event-driven, NOT a direct service call.** `JourneyItemService` already injects `DocumentService` (`JourneyItemService.java:9,39`); making `DocumentService` call `JourneyItemService` back creates a constructor-injection cycle, which Spring Boot 4 / Spring Framework 7 prohibits (the app won't boot). Instead: - `DocumentService.deleteDocument` publishes a domain event (`DocumentDeletingEvent`, payload = **`documentId` UUID only**, no entity references across the boundary) via `ApplicationEventPublisher` (one-line `@RequiredArgsConstructor` injection) **before** `documentRepository.deleteById(id)` on `DocumentService.java:1104`, inside the same `@Transactional` boundary. - A small `@Component` `JourneyItemDocumentDeleteListener` in the `geschichte.journeyitem` package carries a **synchronous** `@EventListener` that calls the repository delete directly (no `JourneyItemService` method — routing through the service would re-introduce the audit emission the audit decision explicitly bypasses). Keep it a thin delegator. Use a plain `@EventListener` — it runs in the publisher's thread and transaction. Do **NOT** use `@Async` or `@TransactionalEventListener(AFTER_COMMIT)`: AFTER_COMMIT fires after the FK SET NULL has already 500'd, and async runs outside the delete transaction (breaks AC-5). The listener-phase choice is load-bearing — comment it. - After the listener runs, `deleteById` proceeds; the FK `ON DELETE SET NULL` then nulls the *remaining* note-carrying rows (preserving the placeholder UX). > ⚠️ **This is the first custom domain event in the codebase.** No existing precedent: the only `@EventListener` today is `OcrTrainingService.java:235` on Spring's framework `ApplicationReadyEvent` lifecycle hook; `OcrStreamEvent` is a `sealed interface` streamed to an SSE `Consumer` (not a Spring `ApplicationEvent`), and there are **zero** `ApplicationEventPublisher` usages in `main/`. Establishing a publish/subscribe pattern for cross-domain cleanup is a precedent-setting decision: **write an ADR** in `docs/adr/` (mirror the ADR-036 structure: context → decision → rejected alternatives → consequences). The ADR's *central* content is the load-bearing listener-phase decision (plain `@EventListener`, explicitly NOT `AFTER_COMMIT`, NOT `@Async`), plus the Spring-7 cycle constraint, the `documentId`-only payload boundary, the "documents must not depend on journey" direction, and the rejected alternatives (DB trigger / RESTRICT / relaxing the CHECK). **Do NOT** relax `chk_journey_item_not_empty` and **do NOT** add a DB trigger — the CHECK is a real invariant and trigger logic is invisible to Java devs. No schema change → **no new migration** and **no `db-orm.puml` / `db-relationships.puml` update**. The only doc deliverable is the ADR. **Repository** — explicit `@Modifying @Query` JPQL (a *derived* `deleteByDocumentIdAndNoteIsNull` breaks like `existsByGeschichteIdAndDocumentId` did, because the transient `getDocumentId()` getter (`JourneyItem.java:48`) makes Spring Data resolve `documentId` as an unmappable attribute — trap documented at `JourneyItemRepository.java:33-44`): ```java @Modifying(clearAutomatically = true) @Query("DELETE FROM JourneyItem i WHERE i.document.id = :documentId AND (i.note IS NULL OR i.note = '')") int deleteNoteLessByDocumentId(@Param("documentId") UUID documentId); ``` Use `i.document.id` (the real association path), never `i.documentId`. Return `int` (rows deleted) — useful for a WARN log in the listener and asserted in the idempotency test. `clearAutomatically = true` is required so AC-2's "note-carrying survives" assertion doesn't read a stale first-level-cache entity. ### Acceptance criteria - **AC-1** Given a note-less journey item whose document is deleted, the item is removed and the document delete succeeds (HTTP 2xx, no 500). - **AC-2** Given a note-carrying item whose document is deleted, the item is retained with `document_id = NULL` and its note unchanged (existing placeholder UX preserved). - **AC-3** Given a note-only item (never had a document), deleting any document does not affect it — item still exists AND note unchanged. - **AC-4** Given a document used in multiple journeys, AC-1/AC-2 apply independently per referencing item (the asymmetric case: journey A note-less → gone, journey B note-carrying → survives as placeholder). - **AC-5** All cleanup occurs within the document-delete transaction; if the delete fails, no journey items are removed (the listener's JPQL delete and the document delete roll back as one unit). - **AC-6** A note-less item with an empty-string note (`note = ''` — satisfies the CHECK because NOT NULL, but dodges the service-layer `normalizeNote`) must not re-trigger the 500; it is cascaded. A whitespace-only note (`note = ' '`) is **note-carrying** and is preserved (the predicate is `= ''`, not `TRIM(note) = ''` — pin this boundary so nobody widens it). - **AC-7** The curator receives **no error and no notification** when their journey silently loses a note-less item. Assert **both halves**: the document-delete audit entry exists and is unchanged, AND no journey-item audit row is written. (Asserting only the absence would still pass if a regression dropped *all* audit.) ### Tests (start red, real Postgres / Testcontainers only — the constraint never fires under H2) - ⚠️ **All new tests must delete through `DocumentService.deleteDocument(id)`, NOT `documentRepository.deleteById(id)`.** The existing journey integration tests (`JourneyItemIntegrationTest.java:216`) delete via the repository, bypassing the service where the event is published — copying that pattern makes the listener never fire and the test pass for the wrong reason (the single biggest green-but-wrong risk here). Autowire `DocumentService`. - **AC-2 regression guard:** update `deleting_document_sets_item_document_to_null_not_delete_item` to route through `DocumentService` (or add a sibling that does) — the repo-path version stays green even if the service path breaks the placeholder behavior. - `deleting_document_linked_via_note_less_item_deletes_item_not_500` — headline (AC-1); confirm it fails with the constraint error before writing the listener. - AC-3 positive: note-only item untouched after an unrelated document delete. - AC-4 asymmetric: same document in two journeys (one note-less, one note-carrying) → first gone, second survives as placeholder. - AC-6: insert `note = ''` via **raw JDBC / `JdbcTemplate`** (the service can't produce it) → assert cascaded (no 500); insert `note = ' '` via raw JDBC → assert preserved. - AC-5 rollback: `@SpyBean`/`@MockBean` the `DocumentRepository`, let the real listener run on real Postgres, then stub `deleteById` to throw a **`RuntimeException`** (Spring `@Transactional` rolls back on RuntimeException by default) *after* the event fires; re-query from Postgres **after `em.clear()`** and assert the note-less items are still present (guards against stale L1 cache hiding a non-rollback). - Idempotency / no-collateral: deleting a document in **zero** journeys returns `0` from `deleteNoteLessByDocumentId`, does not error, and leaves all unrelated journey-item counts unchanged (guards an over-broad JPQL `DELETE` missing the `document.id` predicate — assert the count, not just absence of an exception). ### Sequencing & post-deploy This P1 is a **hard predecessor of #795** — merge #805 first, otherwise the first document delete after #795 ships hits the 500. Enforce the merge order in the PR description and milestone. Post-deploy verification: - Before deploy, capture the baseline: GlitchTip `INTERNAL_ERROR` events on the document-delete path, and/or `count_over_time({...} |= "chk_journey_item_not_empty" [7d])` in Loki. - After deploy, **actively trigger a document delete that would have 500'd pre-fix** (smoke action) so "zero errors" reflects a path actually exercised, not a quiet observation window. The Loki signal goes silent because the violation no longer fires — there is no new log line to add; the *absence* of the existing WARN is the signal. ## Hand-off to #795 (the deleted-document placeholder renderer ships there, not here) #805 ships **no UI**. Seed these as real acceptance criteria in #795 before #805 closes (otherwise they disappear with this issue): - **XSS:** the placeholder note renders via Svelte `{note}` (auto-escaped), **never** `{@html}` — `JourneyItem.note` is stored verbatim (CWE-79 tripwire, `JourneyItem.java:41`). Verify with a test using a `<script>`-bearing note. (Bonus: `{note}` also renders literal `&`/`<`/`>` in real curator prose correctly.) - **Accessibility (60+ audience):** the "deleted document" placeholder row needs a real text label (not italic/color alone — not an accessible status cue), an `aria-label` on the remove control, and a ≥44px touch target on that control. ## Notes - The "deleted-document placeholder" UX (`document_id IS NULL` items rendered as italic removable rows) depends on `ON DELETE SET NULL` working for items that **do** have a note. The fix preserves this for note-carrying items.
marcel added the P1-highbug labels 2026-06-11 13:16:34 +02:00
Author
Owner

Implementation complete — PR #806

What was implemented

Root fix (44869d64):

  • DocumentDeletingEvent(UUID documentId) record — first custom domain event in the codebase
  • JourneyItemDocumentDeleteListener — plain @EventListener (load-bearing choice, see ADR-038) calls journeyItemRepository.deleteNoteLessByDocumentId directly, avoiding the Spring Framework 7 constructor-injection cycle prohibition
  • JourneyItemRepository.deleteNoteLessByDocumentId — explicit @Modifying @Query JPQL with i.document.id path (avoids the transient-getter trap), predicate (note IS NULL OR note = ''), clearAutomatically = true
  • DocumentService.deleteDocument — publishes event before deleteById, adds DOCUMENT_DELETED audit entry
  • AuditKind.DOCUMENT_DELETED — new kind; gives AC-7 its positive assertion anchor
  • JourneyItemIntegrationTest — 2 existing tests updated to route through DocumentService (were bypassing service via documentRepository.deleteById)

ADR (eefc67bd):

  • docs/adr/038-domain-event-driven-journey-item-cleanup.md

Tests (all Testcontainers/real Postgres, all delete via DocumentService)

Test AC
deleting_document_linked_via_note_less_item_deletes_item_not_500 AC-1
deleting_document_preserves_note_carrying_item_as_placeholder AC-2
deleting_document_does_not_affect_note_only_item AC-3
deleting_document_applies_independently_per_referencing_item AC-4
listener_deletes_roll_back_when_document_delete_fails AC-5
empty_string_note_item_is_cascaded_whitespace_only_note_is_preserved AC-6
deleting_document_in_zero_journeys_returns_no_collateral idempotency
deleting_document_emits_document_audit_but_no_journey_item_audit AC-7

All 23 affected tests pass. Build clean.

## Implementation complete — PR #806 ### What was implemented **Root fix** (`44869d64`): - `DocumentDeletingEvent(UUID documentId)` record — first custom domain event in the codebase - `JourneyItemDocumentDeleteListener` — plain `@EventListener` (load-bearing choice, see ADR-038) calls `journeyItemRepository.deleteNoteLessByDocumentId` directly, avoiding the Spring Framework 7 constructor-injection cycle prohibition - `JourneyItemRepository.deleteNoteLessByDocumentId` — explicit `@Modifying @Query` JPQL with `i.document.id` path (avoids the transient-getter trap), predicate `(note IS NULL OR note = '')`, `clearAutomatically = true` - `DocumentService.deleteDocument` — publishes event before `deleteById`, adds `DOCUMENT_DELETED` audit entry - `AuditKind.DOCUMENT_DELETED` — new kind; gives AC-7 its positive assertion anchor - `JourneyItemIntegrationTest` — 2 existing tests updated to route through `DocumentService` (were bypassing service via `documentRepository.deleteById`) **ADR** (`eefc67bd`): - `docs/adr/038-domain-event-driven-journey-item-cleanup.md` ### Tests (all Testcontainers/real Postgres, all delete via `DocumentService`) | Test | AC | |---|---| | `deleting_document_linked_via_note_less_item_deletes_item_not_500` | AC-1 | | `deleting_document_preserves_note_carrying_item_as_placeholder` | AC-2 | | `deleting_document_does_not_affect_note_only_item` | AC-3 | | `deleting_document_applies_independently_per_referencing_item` | AC-4 | | `listener_deletes_roll_back_when_document_delete_fails` | AC-5 | | `empty_string_note_item_is_cascaded_whitespace_only_note_is_preserved` | AC-6 | | `deleting_document_in_zero_journeys_returns_no_collateral` | idempotency | | `deleting_document_emits_document_audit_but_no_journey_item_audit` | AC-7 | All 23 affected tests pass. Build clean.
Sign in to join this conversation.
No Label P1-high bug
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#805