다크모드
수납 집합 완납 fan-out — BE 참고
독자: BE 개발자/AI. 콘솔 프로토의 집합(유닛/멤버) 완납 분해·fan-out 정책·불변식·알려진 한계. 정본 spec:
docs/superpowers/specs/2026-06-24-collecting-aggregate-fullpay-policy-design.md. 관련: [[tenancy-axes-billing.md]] · [[collecting-detail.md]] · [[demo-seed-factory.md]].
원칙
정산(미수·연체료·충당·선수금)은 계약(납부자) 단위. 유닛/멤버 집합 레벨엔 완납만 허용(모호성 0). 과소·과대납은 계약 단위.
API (useCollectingFullPayment)
fullPaymentBreakdown(axis, key, scope) → { axis, key, scope, lines:[{contractCode, memberCode, label, payable}], total }axis∈unit|member.scope∈current|forwarded|all→forwardingByContractrow의current|forwarding|total필드.payable > 0라인만 포함(완납·0원 계약 제외). 계약 enum =unitOccupants/memberHoldings의 distinctcontractCode.
commitFullPayment(breakdown, {receivedAt, method}) → {receiptIds, count, total}— 라인마다 fullrecordReceipt({contractCode, memberCode, amount:payable, identified:true}). 충당(FIFO·연체료우선)은recordReceipt내부.
불변식
Σ lines.payable = bd.total(계약별 받을금액 합산). Task 4b 이후forwardingBy{Unit|Member}(key)도 소속 계약 행을 집계하므로 축 레벨 total과 정확히 일치(과거buildRow withVat독립 반올림으로 ≤수원 drift가 있었으나 집계 단일소스로 제거). 3축 Σ도 정확 일치.- 완납 커밋 후 각 계약
forwardingByContract(contractCode).total = 0, 그 계약이 속한 유닛/멤버 행도 즉시 반영(집계 파생). - 되돌리기:
receiptIds마다cancelReceipt(id)→ forwarding 복원.
인라인 계약 수납 (계약 행 quick-collect)
계약 탭 atom 행 [수납] 버튼 및 유닛/멤버 펼침 계약 행 [수납] 버튼의 인라인 quick-collect도 동일하게 recordReceipt(contractCode) 단일 경로를 사용한다. 별도 BE API 추가 없음. 충당(FIFO·연체료우선)·선수금 처리는 recordReceipt 내부에서 기존과 동일하게 동작.
제거된 경로
유닛/멤버 임의금액 useForwarding.collect('unit'/'member', amount)(단건·batch) — 납부자 모호로 폐지. 계약축 collect/recordReceipt만 정산 진입.
실서비스 메모
fan-out N건 영수증은 입금 1건이 여러 채권에 충당되는 구조. 실 BE는 입금전표 1건 ↔ 충당 N행(영수증)으로 모델링 권장.
알려진 한계 — 승계 시나리오
유닛/멤버 행은 소속 계약(배분 line 기준)을 집계한다. 활성 계약과 종료(부과종료) 계약이 같은 유닛/멤버에 공존하는 승계 시나리오에서, 종료 계약은 배분 line이 없어 contractsOfUnit/Member(line 기준)에 잡히지 않아 그 유닛/멤버 집합 행 미수에서 누락될 수 있다. 데모 시드엔 미발현. 근본 해결 = 종료 계약의 contract→unit/member 매핑(BE 모델). 현재는 활성 계약 전무 유닛/멤버만 PRIOR_PERIODS 폴백.