Skip to content

BE 참고 — 미수금 연령분석(AR Aging) 집계 정의 · 불변식 · 데이터 소스

독자: BE 개발자/AI. 미수금 연령분석 화면(/service-charge/actual/console/forwarding/brought-forward)의 집계 정의·roll-forward 항등식·버킷 합 불변식·3축 일관·차기이월=다음기 E1 개시분개 항등·데이터 소스(useForwarding period)를 정의한다. 정본 연계: 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)

차수(월분) 버킷은 전 계약의 periodslabel(차수 식별자)로 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 구조

useReceivableAginguseForwarding().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.toAgingRowkind='current'만 필터해 당기발생·회수·조정을 추출한다.

확정규칙 2: 각 period의 outstandingbilled − 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)
croll-forward 항등전기이월 + 당기발생 − 회수 − 조정 == 차기이월 (행 단위)
d3축 일관summary('contract').총차기이월 == summary('unit').총차기이월 == summary('member').총차기이월
e연체 집계summary.연체행수 == Σ isAnomalous, summary.최장연체차수 == max(overdueCount)
fE1 항등 (Track 3)Σ(E1전표.total) == summary.총차기이월 == 차기 총전기이월

모든 불변식은 src/composables/__tests__/receivableAging.spec.js에서 검증한다.


8. 미구현·확장 계획

항목상태설명
roll-forward 런타임 엔진❌예정월말 미수→차월 이월 자동승격. 현재는 시드 priorPeriods 고정. 실 BE에서 월 경계 모델 필요
actual 에디션 전용provisional 에디션 aging은 현재 범위 밖
드릴다운 액션 배선❌예정aging 행 → 수납 콘솔 딥링크 · 계정 허브 시트 연결
날짜 기준 필터❌예정조회 기준일 시점 aging 보기 (수납 콘솔 타임머신 패턴 참고)