다크모드
잠정정산(provisional settlement) — BE 핸드오프
2026-07-18 as-built. product owner는
service-charge이며 정기 부과(actual) 4단계 workflow와 분리된 단건 중간정산 계약이다.
경계
필수 상품은 service-charge, 필수 대상은 (company_id, office_id, property_space_id)의 public.property_spaces다. lease_contracts, 임대 권리·상태, service_charge_stage_workflows, confirm_service_charge_facts를 참조하지 않는다. 선택 원본은 source_subject_type/key/label 문자열 세 칼럼뿐이고 FK·lookup이 없다.
정본 migration은 20260718170000_service_charge_provisional_settlement_events.sql이다.
데이터 모델
service_charge_provisional_settlements: mutabledraft. 관리코드·등록코드·프로퍼티 공간 scope는 생성 후 불변이다. 이름·사유·기준일·lookback·buffer·명시적 계산 입력·서버 snapshot·합계·선택 원본 문자열·revision을 가진다.service_charge_provisional_settlement_confirmations: settlement당 1개인 immutable terminal Fact. 확정 revision, calculation version/snapshot/total, actor/time/request key를 보존한다.service_charge_provisional_settlement_events: created/updated/confirmed/cancelled append-only 감사행.private.service_charge_provisional_settlement_commands: command별 request payload/response ledger.(company_id, command_name, request_key)가 exact retry를 보장한다.
상태는 draft → confirmed 또는 draft → cancelled뿐이다. 두 terminal 상태는 update와 재전이를 모두 거부한다. 삭제·reopen·재확정 버전은 이 계약에 없다.
계산 계약 v1
private.calculate_service_charge_provisional_snapshot_v1가 브라우저와 무관하게 계산한다. 입력은 1120개 operator-owned line이며 각 line은 고유 code, name, 36개의 JSON number 단가 이력, 0 이상 quantity를 가진다.taxable|exempt, area|usage, 1
- 평균 단가: 최근
lookback_months개 산술평균 - 추산 단가: 평균 ×
(1 + buffer_basis_points / 10000) - area:
round(추산 단가 × quantity × cutoff day / 실제 month days) - usage:
round(추산 단가 × quantity), 일할 없음 - tax: taxable 공급가액 합 × 정확한 정수 basis points, 기본 1000
area의 day/monthDays는 client 값을 신뢰하지 않는다. cutoff_date에서 윤년과 30/31일을 서버가 파생하고 전달 값이 exact integer로 같아야 한다. 세율·day의 fractional number, 숫자 문자열, 중복 code, 빈 history, history 36개 초과는 안정 오류 service_charge_provisional_calculation_input_invalid 또는 ...rate_history_required로 거부한다.
create/update 때 서버 snapshot과 total을 저장하고 confirm 때 같은 입력을 다시 계산해 stored snapshot/total과 exact 비교한다. 불일치는 service_charge_provisional_snapshot_mismatch다.
RPC와 보안
list_service_charge_provisional_settlements_v1(company, office)→{settlements, spaces}create_..._v1(...),update_..._v1(...)confirm_..._v1(...),cancel_..._v1(...)
모든 command는 auth.uid(), Office membership/role, service-charge entitlement, company/office scope를 서버에서 확인한다. update/terminal command는 expected revision을 행 잠금 안에서 검사한다. 공개 테이블은 RLS를 켰지만 authenticated direct table privilege를 회수했고, browser에는 public RPC execute만 준다. definer 함수는 search_path='', private helper는 PUBLIC/anon/authenticated execute를 회수한다.
관리코드를 비운 create retry는 요청 payload에 nullable 요청값을 기록한 뒤 기존 command를 먼저 조회하고, 신규 insert에서만 코드를 생성한다. 따라서 동일 request key 재시도는 같은 id/code/response를 반환한다. 같은 key의 다른 payload는 conflict다.
선택 연결
확정 Fact와 domain event가 먼저 커밋된다. 그 뒤 platform_integration_events에 service-charge.provisional-settlement-confirmed.v1 중립 이벤트를 best-effort enqueue한다. accounting/e-document delivery를 요청하거나 자동 전기·자동 발행하지 않는다. event enqueue 예외는 Fact를 롤백하지 않는다.
검증
- 실제 PostgreSQL rollback:
supabase/tests/service_charge_provisional_settlement_smoke.sql - 구조:
serviceChargeProvisionalSettlementMigration.spec.js - repo:
serviceChargeProvisionalSettlementRepo.spec.js
smoke는 manager success, unauthorized/cross-Office denial, 빈 관리코드 retry, request conflict, optimistic conflict, label-only source 거부, malformed/fractional 계산 입력, 윤년, terminal 전이, direct write, Fact append-only와 optional event를 실행한다.