Skip to content

수납 집합 완납 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 }
    • axisunit|member. scopecurrent|forwarded|allforwardingByContract row의 current|forwarding|total 필드.
    • payable > 0 라인만 포함(완납·0원 계약 제외). 계약 enum = unitOccupants/memberHoldings의 distinct contractCode.
  • commitFullPayment(breakdown, {receivedAt, method}) → {receiptIds, count, total} — 라인마다 full recordReceipt({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 폴백.