다크모드
BE 참고 — 미수금 연령분석(AR Aging) 집계 정의 · 불변식 · 데이터 소스
독자: BE 개발자/AI. 미수금 연령분석 화면(
/service-charge/actual/console/forwarding/brought-forward)의 집계 정의·roll-forward 항등식·버킷 합 불변식·3축 일관·차기이월=다음기 E1 개시분개 항등·데이터 소스(useForwardingperiod)를 정의한다. 정본 연계:src/composables/useReceivableAging.js,src/composables/useForwarding.js, FE 핸드오프docs/handoff/frontend/receivable-aging-report.md. 표기: as-built(현재 프로토 동작) vs 확정규칙(BE 구현 시 따를 규칙). 다르면 명시한다.
1. 집계 정의 — 5개 필드
| 필드 | 정의 | 원천 |
|---|---|---|
| 전기이월 | 이전 기(기간)에서 이월된 미납 잔액 — 이월 차수 outstanding의 합계 | forwardingRow.forwarding |
| 당기발생 | 당기 차수에 새로 부과된 금액 | periods.find(kind='current').billed |
| 회수 | 당기 차수 내 수납·충당으로 감소한 금액 | periods.find(kind='current').collected |
| 조정 | 당기 차수 내 감면·조정으로 감소한 금액 | periods.find(kind='current').adjusted |
| 차기이월 | 이번 기 마감 후 다음 기로 이월되는 미납 잔액 | forwardingRow.total |
회수·조정 스코프 확정규칙: 회수와 조정은 당기 차수(kind='current')에서만 집계한다. 전기이월에는 이월 차수들의 충당분이 이미 반영돼 있으므로(이월 차수별 outstanding = billed − reduced), 이월 차수 회수를 별도 차감하면 중복차감이 발생한다.
2. roll-forward 항등식 (불변식 c)
전기이월 + 당기발생 − 회수 − 조정 = 차기이월이 항등식은 당기 미과오납(당기발생 ≥ 회수+조정)일 때만 성립한다. useForwarding가 차기이월 = max(0, 당기 billed−collected−adjusted) + 전기이월로 당기 미수를 0에서 클램프 → 차기이월은 전기이월 미만으로 내려가지 않으며(음수 불가), 당기 과오납 시 클램프가 항등을 깬다(LHS < RHS, 과오납분). 과오납은 aging 이전에 선수금(0259)으로 정리해야 한다. (런타임: FIFO collect()는 차수 outstanding을 상한으로 충당 → 과오납 미발생; 비클램프 adjust()만 유발 가능.)
확정규칙: BE가 forwarding(전기이월)·total(차기이월)·당기 period를 제공할 때, 위 항등이 계약 단위에서 성립하도록 보장해야 한다. useReceivableAging.spec.js에서 매 배포마다 검증된다.
3. 버킷 합 불변식 (불변식 b)
차수(월분) 버킷은 전 계약의 periods를 label(차수 식별자)로 group-by해 합산한다.
Σ(bucket.outstanding) = summary.총차기이월 = Σ(forwardingRow.total)세 경로가 동일한 값을 주어야 한다. useReceivableAging.spec.js에서 버킷 합과 forwardingByContract() 직접 순회 합을 독립 2경로로 교차 검증한다(동어반복 회피).
4. 3축 일관 불변식 (불변식 d)
agingSummary('contract').총차기이월
= agingSummary('unit').총차기이월
= agingSummary('member').총차기이월계약·유닛·멤버는 같은 부과 데이터의 서로 다른 group-by 투영이므로 총액이 동일해야 한다.
확정규칙: BE가 3축 각각의 forwarding 집계를 제공할 때, 총차기이월 합산이 축 간 동일하도록 보장해야 한다. 계약 복수 유닛 귀속 등 데이터 모델 이상이 있으면 3축 일관이 깨진다.
5. 차기이월 = 다음 기 E1 개시분개 합 (Track 3 연결)
차기이월은 단순 집계 수치가 아니라 다음 청구기의 E1 전기이월 미수 개시분개 총액과 연결된다.
5-1. E1 전기이월 미수 개시분개 형태
이월 outstanding이 있는 계약 하나당 1개 전표(bj-open-{contractCode}):
DR 미수관리비(0108) = forwardingRow.forwarding (공급대가)
CR 이월이익잉여금(0375) = Σ byComponent(부가세 키 제외) [net > 0일 때]
CR 부가세예수금(0255) = byComponent.부가세 [vat > 0일 때]- 재무상태표 한정 분개: 자산↑ = 자본↑ + 부채↑. 당기 손익(IS)·당기순이익 무영향.
- 인식기준 무관: 원금 미수는 발생주의 기준(E2 청구 시 항상 인식). E3(연체료)만 인식기준 게이트, E1은 게이트 없음.
5-2. 항등 (Track 3)
Σ( E1전표(bj-open-{cc}).total | cc: 이월 outstanding > 0 )
= agingSummary('contract').총차기이월
= 차기 전기이월 개시 잔액이 항등은 모집단 내 과오납이 없을 때만 성립한다. 당기 과오납이 발생해 그 계약의 차기이월이 전기이월 미만으로 클램프되면, E1 전표 총합이 총차기이월보다 작아진다(적수 누락). 현재 기의 총차기이월이 다음 기의 총전기이월로 이어진다. roll-forward 런타임 엔진이 구현되면 이 값을 자동으로 다음 기 E1 전표 발행 금액으로 사용한다.
5-3. VAT 주의 (BE 실 운영 시)
E1이 0255 부가세예수금을 개시로 재수립하는 것은 "과거 청구 부가세가 미신고" 전제다. 실 운영에서 과거 청구분 부가세가 이미 신고·납부됐다면 0255 재수립은 부채 과대계상이 된다. roll-forward 실엔진 도입 시, 개시 부가세 대변은 신고이력에 따라 (a) 미신고분만 0255 / (b) 신고완료분은 별도 정산계정으로 분기해야 한다.
6. 데이터 소스 — useForwarding period 구조
useReceivableAging은 useForwarding().forwardingByContract/Unit/Member() 반환 행을 소비한다. BE가 실 API를 제공할 때 아래 형태를 충족해야 한다.
forwardingRow 형태
js
{
key: string, // 축 식별자
name: string, // 표시명
forwarding: number, // 전기이월 미납 잔액 (이월 차수 outstanding 합)
total: number, // 차기이월 = 마감 미수 잔액
overdueCount: number, // 연체 차수 수
periods: Period[] // 차수별 상세 (아래 Period 형태)
}Period 형태
js
{
label: string, // 차수 식별자 (예: '2025-11', '2025-12')
kind: 'forwarded' | 'current', // 'forwarded' = 이월 차수, 'current' = 당기 차수
billed: number, // 그 차수 부과 금액
collected: number, // 그 차수 수납·충당 금액
adjusted: number, // 그 차수 감면·조정 금액
outstanding: number, // 그 차수 미납 잔액 = billed − collected − adjusted
dueDate: string, // 납기일
status: string // 상태 표시문자열
}확정규칙 1: 이월 차수는 kind='forwarded', 당기 차수는 kind='current'로 구분해야 한다. useReceivableAging.toAgingRow가 kind='current'만 필터해 당기발생·회수·조정을 추출한다.
확정규칙 2: 각 period의 outstanding은 billed − collected − adjusted와 일치해야 한다. forwardingRow.forwarding = Σ(period.outstanding | kind='forwarded'), forwardingRow.total = Σ(period.outstanding).
확정규칙 3: label은 차수 정렬 기준으로 사용된다(문자열 오름차순 → 오래된 차수가 먼저 표시). ISO 날짜 형식('YYYY-MM') 권장.
7. 불변식 요약
| 번호 | 불변식 | 수식 |
|---|---|---|
| a | 패스스루 정합 | agingRow.전기이월 == forwardingRow.forwarding, agingRow.차기이월 == forwardingRow.total |
| b | 버킷 합 | Σ(bucket.outstanding) == summary.총차기이월 == Σ(forwardingRow.total) |
| c | roll-forward 항등 | 전기이월 + 당기발생 − 회수 − 조정 == 차기이월 (행 단위) |
| d | 3축 일관 | summary('contract').총차기이월 == summary('unit').총차기이월 == summary('member').총차기이월 |
| e | 연체 집계 | summary.연체행수 == Σ isAnomalous, summary.최장연체차수 == max(overdueCount) |
| f | E1 항등 (Track 3) | Σ(E1전표.total) == summary.총차기이월 == 차기 총전기이월 |
모든 불변식은 src/composables/__tests__/receivableAging.spec.js에서 검증한다.
8. 미구현·확장 계획
| 항목 | 상태 | 설명 |
|---|---|---|
| roll-forward 런타임 엔진 | ❌예정 | 월말 미수→차월 이월 자동승격. 현재는 시드 priorPeriods 고정. 실 BE에서 월 경계 모델 필요 |
| actual 에디션 전용 | ✅ | provisional 에디션 aging은 현재 범위 밖 |
| 드릴다운 액션 배선 | ❌예정 | aging 행 → 수납 콘솔 딥링크 · 계정 허브 시트 연결 |
| 날짜 기준 필터 | ❌예정 | 조회 기준일 시점 aging 보기 (수납 콘솔 타임머신 패턴 참고) |