다크모드
계약관리 백엔드 핸드오프
범위와 소유권
기본 마이그레이션 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,renewaleffective_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버전을 appendrenew_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_version과revision만 증가하고 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-managementcontract_management_manager_requiredcontract_management_revision_conflictcontract_management_idempotency_conflictcontract_management_party_invalidcontract_management_primary_party_requiredcontract_management_update_requires_draftcontract_management_lifecycle_input_invalidcontract_management_change_requires_activecontract_management_change_reason_requiredcontract_management_amendment_effective_date_invalidcontract_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 미생성을 검사한다.