Files
mdm-ordinis/ordinis-migrations
Zimin A.N. e438c4c8c8 feat(approval): Approval Workflow v2 Phase 0 — schema + skeleton API
Approval Workflow v2 epic, Phase 0 foundation. Maker-checker pattern для
критичных справочников через record_drafts отдельную таблицу (Approach A
из design doc).

Decisions made (auto, per design doc recommendations):
- D1=A: per-dict policy через колонки на dictionary_definitions
  (approval_required + approval_min_role).
- D2=A: legacy POST на approval-required dict вернёт 409 (Phase 1 wire'ит
  в DictionaryRecordService).
- D3=A: pool model для reviewer'ов — все ordinis:approver видят queue.
- D4=A: rejected drafts kept (status='rejected'); compliance audit trail.

Migration 0019:
- record_drafts table:
  * id, dictionary_id, business_key, parent_record_id, operation
  * data (jsonb), geometry, proposed_valid_from/to
  * status, maker_id, maker_comment, submitted_at
  * reviewer_id, reviewed_at, review_comment, version
- 4 constraints:
  * status_check: pending/approved/rejected/withdrawn
  * operation_check: CREATE/UPDATE/CLOSE
  * no_self_approve: maker_id != reviewer_id
  * one_pending_per_key: EXCLUDE WHERE status=pending
    (concurrent edits → последний wins на submit, prevents merge conflicts)
- 3 indices: pending_by_dict, by_maker, pending_global.
- dictionary_definitions: +approval_required (BOOLEAN, default false),
  +approval_min_role (VARCHAR 64, optional Keycloak role).

Backend:
- Entity RecordDraft + Repository с JPQL queries для:
  * findPendingByKey (single pending lookup)
  * findPending (global queue для approver pool, paginated)
  * findPendingByDictionary (per-dict list)
  * findByMaker (own drafts tracking, paginated)
- DTO SubmitDraftRequest, DraftResponse.
- DraftService skeleton:
  * submit() — creates pending draft. 409 на exclusion violation.
  * findById, listPendingByDictionary, listPendingGlobal — read-only.
  * approve / reject / withdraw / updateDraft — Phase 1 stubs (комментарии).
- DraftController с 4 endpoints:
  * POST /api/v1/dictionaries/{name}/records/drafts — submit
  * GET /api/v1/dictionaries/{name}/records/drafts — list per dict
  * GET /api/v1/drafts/{id} — single draft
  * GET /api/v1/admin/reviews/pending — global queue (paginated)

Tests:
- DraftServiceTest 6 unit tests:
  * submit creates with PENDING status
  * 409 на duplicate pending business_key (exclusion constraint)
  * invalid WKT → 400
  * dict not found → 404
  * findById unknown → 404
  * listPendingByDictionary returns multiple pending
- StubDraftRepo simulates DB exclusion constraint в memory.

Verify:
- mvn -pl ordinis-rest-api -am test: 112/112 PASS (+6 new).
- mvn -P e2e -pl ordinis-app -am test: 20/20 PASS — migration 0019
  applies clean, no regression.
- helm/yaml configs не трогали — Phase 0 чистый schema + code.

Phase 1 (next session) — full lifecycle:
- DraftService.approve(): SERIALIZABLE tx с COPY draft → live + outbox
  RecordCreated/Updated/Deleted event + audit. Validate against schema
  ON APPROVE (могла schema измениться с момента submit'а).
- reject() — mark status=rejected, kept (D4).
- withdraw() — maker отзывает свой pending.
- DictionaryRecordService.create/update/close: pre-check
  approval_required → 409 draft_required. JWT subject → maker_id.
- Reviewer queue + diff viewer admin UI.

Source: ~/.gstack/projects/claude/zimin-main-design-approval-workflow-v2-20260507-121632.md
2026-05-08 13:08:21 +03:00
..