Skip to content

계약관리 백엔드 핸드오프

범위와 소유권

기본 마이그레이션 20260718280000_contract_management_core.sql과 수명주기 확장 20260719030000_contract_lifecycle_versions.sql의 product owner는 contract-management다. 이 상품은 범용 계약의 정본만 소유하며 Billing, CRM, 임대차관리의 사실을 직접 소유하거나 해당 테이블을 FK로 참조하지 않는다.

product_catalog에는 contract-management가 베타 설명, 기본가 0원·단가 0원으로 등록된다. 모든 public RPC와 RLS는 Company 계약 + Office entitlement를 함께 확인한다.

데이터 모델

contract_management_contracts

계약 aggregate root다. Company/Office, 최초 고정 관리코드·등록번호·source identity, 상태, 현재 버전 포인터, revision만 가변적으로 보유한다.

  • 상태: draft → active → ended, 또는 draft → cancelled
  • management_code, registration_number, Company/Office와 source snapshot은 생성 후 불변
  • revision은 update/transition마다 1 증가
  • 한 Office에서 관리코드는 고유

contract_management_versions

계약 조건의 불변 버전이다. Update·amendment·renewal은 기존 행을 수정하지 않고 current_version + 1을 추가한다. UPDATE/DELETE trigger가 모든 직접 변경을 거부한다.

유형은 sales, purchase, service, construction, lease, subscription, maintenance, membership, other다. lease는 일반 계약 분류일 뿐 임대차관리 상품과 hard FK가 없다.

버전 수명주기 메타데이터는 다음과 같다.

  • change_kind: initial, draft_revision, amendment, renewal
  • effective_on: 해당 버전 조건이 적용되는 날짜
  • change_reason: amendment/renewal의 필수 사유
  • supersedes_version_number: 바로 앞에서 대체하는 버전 번호

기존 버전은 어떤 명령도 갱신하지 않는다. 현재 버전 포인터만 새 버전으로 이동하며, version number와 supersedes_version_number가 한 계약 안의 선형 이력을 만든다.

마이그레이션 시 기존 v1은 initial, 기존 v2 이상은 draft_revision으로 채우고 effective_on=starts_on으로 보존한다. v2 이상은 바로 전 version number를 supersede하도록 연결한다.

contract_management_parties

Party SSOT인 master_parties.id를 FK로 참조한다. 버전마다 역할·주 당사자 여부와 당시 표시 snapshot을 불변 저장한다. RPC는 다음을 검증한다.

  • Party가 같은 Company 소속이고 활성 상태
  • 해당 Office의 활성 office_party_assignments 존재
  • 역할 enum 유효
  • 한 버전에 주 당사자가 정확히 한 명

private.contract_management_commands

명령 ledger다. (company_id, request_key)가 유일하고 request payload가 완전히 같은 경우 저장된 response를 재생한다. 같은 키에 다른 payload가 오면 contract_management_idempotency_conflict다. advisory transaction lock으로 동시 최초 요청도 직렬화한다.

Public RPC 계약

  • list_contract_management_contracts(company_id, office_id)
  • create_contract_management_contract(...)
  • update_contract_management_contract(..., expected_revision, request_key)
  • activate_contract_management_contract(..., expected_revision, request_key)
  • end_contract_management_contract(..., ended_on, reason, expected_revision, request_key)
  • cancel_contract_management_contract(..., reason, expected_revision, request_key)
  • amend_contract_management_contract(...): 변경 적용일·사유와 새 조건/당사자를 받아 활성 계약에 amendment 버전을 append
  • renew_contract_management_contract(...): 기존 종료일 다음 날부터 시작하는 새 기간·사유와 새 조건/당사자를 받아 활성 계약에 renewal 버전을 append

Public wrapper는 invoker이고, private implementation은 empty search path의 security definer다. authenticated는 public table SELECT만 가능하며 INSERT/UPDATE/DELETE grant가 없다. RLS는 can_read_contract_management_office에서 Office 접근권한과 상품 entitlement를 함께 확인한다.

Revision과 상태 불변식

  • 모든 mutation은 expected_revision 일치를 요구한다.
  • update는 draft만 가능하고 draft_revision 불변 버전을 append한다.
  • activate는 draft만 가능하다.
  • amendment와 renewal은 active만 가능하고 현재 버전을 덮어쓰지 않는다.
  • amendment의 적용일은 현재 활성 기간 안의 유효한 날짜여야 한다.
  • renewal의 시작일은 현재 버전 종료일의 정확히 다음 날이어야 하며 종료일은 시작일보다 빠를 수 없다.
  • amendment/renewal은 사유가 필수이며 expected_revision으로 동시 변경을 거부한다.
  • 성공 시 contract root의 current_versionrevision만 증가하고 status는 active를 유지한다.
  • end는 active만 가능하며 종료일은 현재 버전 시작일보다 빠를 수 없다.
  • cancel은 draft만 가능하고 사유가 필수다.
  • 종료/취소 상태는 terminal이다.

Source snapshot과 loose coupling

Create는 source_product_key, source_aggregate_type, source_aggregate_id, source_snapshot을 선택적으로 받는다. 세 identity 값은 all-or-none이며 snapshot은 JSON object다. CRM v1 handoff는 crm-deal-contract-draft-handoff.v1 snapshot을 사용하지만 계약 코어는 CRM 스키마를 해석하지 않는다.

성공한 로컬 명령은 private.enqueue_platform_integration_event로 provider-neutral event를 남긴다. 코어는 target delivery를 요청하지 않는다. 따라서 consumer·adapter가 하나도 없어도 로컬 transaction은 완결된다. 추후 Billing/CRM adapter가 명시적으로 delivery route를 등록해야 한다.

amendment와 renewal은 각각 contract_management.contract_amended, contract_management.contract_renewed event와 contract-management.contract-amended.v1, contract-management.contract-renewed.v1 schema를 사용한다. payload는 현재 version metadata와 전체 snapshot을 담지만 target product delivery는 만들지 않는다.

계약서 PDF·해시·타임스탬프·전자문서 보관은 계약 aggregate가 소유하지 않는다. 플랫폼 코어 document-trust에 현재 불변 버전 snapshot을 선택적으로 넘긴다. 두 영역 사이 FK는 없으며 상세 계약은 문서 신뢰 request가 없어도 정상 조회·수정·상태 전이된다. 정본은 docs/handoff/backend/document-trust.md다.

감사와 오류

모든 create/update/activate/end/cancel은 operational_audit_events에 before/after와 request payload를 기록한다. 주요 오류 코드는 다음과 같다.

  • product_not_entitled:contract-management
  • contract_management_manager_required
  • contract_management_revision_conflict
  • contract_management_idempotency_conflict
  • contract_management_party_invalid
  • contract_management_primary_party_required
  • contract_management_update_requires_draft
  • contract_management_lifecycle_input_invalid
  • contract_management_change_requires_active
  • contract_management_change_reason_required
  • contract_management_amendment_effective_date_invalid
  • contract_management_renewal_term_invalid

검증

  • SQL smoke: supabase/tests/contract_management_core_smoke.sql
  • 구조 테스트: src/composables/__tests__/contractManagementMigration.spec.js
  • 수명주기 SQL smoke: supabase/tests/contract_lifecycle_versions_smoke.sql

SQL smoke는 exact replay/conflict, draft/amendment/renewal version append, amendment 적용일, renewal 다음 날 규칙, 과거 버전 불변, 상태 전이, 타 Company Party 거부, entitlement, direct write 차단, audit/outbox와 delivery 미생성을 검사한다.