feat(approval): Approval Workflow v2 Phase 3 — e2e suite + 0020 case fix

Phase 3 e2e coverage (8 tests, ApprovalWorkflowE2ETest):
- happyFlow: maker submit → checker approve → live record + outbox event
- rejectFlow: keeps draft REJECTED, no live record, re-approve blocked
- selfApprove: same JWT subject blocked (self_approve_forbidden / self_reject_forbidden)
- twoPendingDrafts: exclusion constraint → 409 draft_already_pending
- directCRUD on approval_required dict: POST/PUT/DELETE → 409 draft_required (D2=A)
- regression: dict без approval_required — direct CRUD по-прежнему работает
- withdrawByMaker: maker может, reviewer не может (403 not_draft_maker)
- rejectWithoutComment: 400 reject_reason_required

E2e exposed real Phase 0 bug:
Hibernate @Enumerated(EnumType.STRING) пишет UPPERCASE ('PENDING'), а
Liquibase 0019 check constraint + exclusion WHERE использовали lowercase
('pending'). Каждый insert проваливался на check, маппился в
DataIntegrityViolation, и DraftService.submit маскировал его в
draft_already_pending — скрывая реальную причину.

Fixes:
1. Migration 0020: пересоздание record_drafts_status_check + exclusion
   с UPPERCASE значениями (matches enum serialization).
2. DraftService.submit: только SQLSTATE 23P01 (exclusion violation)
   маппится в draft_already_pending. Остальные DataIntegrity (check, FK,
   NOT NULL) пробрасываются как-есть — иначе schema bugs продолжат
   маскироваться.
3. DraftServiceTest StubDraftRepo: оборачивает SQLException 23P01 в
   DataIntegrityViolation cause chain — отражает прод поведение.

Test counts:
- ordinis-rest-api unit: 119/119 PASS
- ordinis-app e2e: 28/28 PASS (был 20, +8 новых ApprovalWorkflowE2ETest)
This commit is contained in:
Zimin A.N.
2026-05-08 13:52:17 +03:00
parent 84a4de4ceb
commit 9ef9b8147e
5 changed files with 462 additions and 5 deletions
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
<!--
Approval Workflow v2 — Phase 3 fix.
BUG (exposed by e2e): Hibernate с @Enumerated(EnumType.STRING) пишет значения
enum'а в UPPER CASE (PENDING, APPROVED, REJECTED, WITHDRAWN). А Liquibase 0019
объявил check constraint + exclusion WHERE с lowercase ('pending'). Любой
insert PENDING draft'а проваливал check constraint, маппился в
DataIntegrityViolationException, и DraftService.submit ловил это как
draft_already_pending — маскируя реальную причину.
Fix: переписать ограничения с UPPERCASE значениями (matches Java enum norm).
Существующих PENDING-row быть не должно (insert никогда не проходил), но на
всякий случай конвертируем lowercase → UPPERCASE до пересоздания constraint'ов.
-->
<changeSet id="0020-1-normalize-existing-status" author="ordinis">
<comment>Normalize any prior lowercase status values to UPPERCASE before recreate.</comment>
<sql>UPDATE record_drafts SET status = UPPER(status) WHERE status != UPPER(status);</sql>
</changeSet>
<changeSet id="0020-2-fix-status-check" author="ordinis">
<comment>Recreate status check constraint with UPPERCASE values matching enum.</comment>
<sql>ALTER TABLE record_drafts DROP CONSTRAINT IF EXISTS record_drafts_status_check;</sql>
<sql>
ALTER TABLE record_drafts
ADD CONSTRAINT record_drafts_status_check
CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED', 'WITHDRAWN'));
</sql>
</changeSet>
<changeSet id="0020-3-fix-exclusion-where" author="ordinis">
<comment>Recreate exclusion WHERE clause to use 'PENDING' (matches enum serialization).</comment>
<sql>ALTER TABLE record_drafts DROP CONSTRAINT IF EXISTS record_drafts_one_pending_per_key;</sql>
<sql>
ALTER TABLE record_drafts
ADD CONSTRAINT record_drafts_one_pending_per_key
EXCLUDE USING btree (
dictionary_id WITH =,
business_key WITH =
) WHERE (status = 'PENDING');
</sql>
</changeSet>
</databaseChangeLog>
@@ -29,5 +29,6 @@
<include file="changes/0017-redis-projection-flag.xml" relativeToChangelogFile="true"/>
<include file="changes/0018-trgm-search-index.xml" relativeToChangelogFile="true"/>
<include file="changes/0019-approval-workflow-v2.xml" relativeToChangelogFile="true"/>
<include file="changes/0020-fix-draft-status-case.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>