Skip to content

BE 참고 — 수납 상세 시트 / 수납처리 액션 + 분개

독자: BE 개발자/AI. 수납 상세 시트(SheetCollectingDetailArt, 진입=collecting 목록 행 클릭 → 공유 ref selectedCollecting{axis,key})의 데이터 모델·충당 알고리즘·연체료 엔진 계약·감면/대손/인식기준 분개 규칙·불변식·엣지케이스를 정의한다. (전용 페이지 라우트 collecting/:axis/:key는 제거됨 — 시트로 단일화.) 정본 연계: 수납 프로세스 캐논 docs/PROCESS-RECEIPTS-CANON-2026-06-19.md(as-built/확정규칙), 연체료 …/specs/2026-06-19-late-fee-interest-engine-design.md, 표현방식 환원 …/specs/2026-06-19-collecting-detail-sheet-revert-design.md(원안 …/2026-06-19-collecting-detail-page-design.md). 표기: as-built(현재 프로토 동작) vs 확정규칙(BE 구현 시 따를 규칙). 다르면 명시한다.


1. 데이터 모델 — 차수 단일 모델

수납 상세는 useCollectingDetail.detailFor(axis, key, asOf)가 세 레이어를 차수별 단일 모델로 조립한다.

  • useForwarding — 원금(outstandingdueDate·kind('forwarded'|'current')·billed·collected
  • useLateFee.lateFeesFor(axis, key, asOf) — 차수별 연체료(fee, breakdown[]), interestEngine 산출
  • useReceipts — 충당·선수금(0259)/가수금(0257) 잔액·원장

detail 형태 (as-built)

{
  identity: { title, member, status('완납'|'미수'), contractCode },
  periods: [{
    label('YYYY-MM'), dueDate, kind('forwarded'|'current'),
    principal,                 // = forwarding.outstanding (잔여 원금)
    principalByTax: { 과세, 면세, 영세 },   // 원금 세구분 분해(Σ === principal)
    lateFee: { total, breakdown[] },
    billed, collected,
    balance(= principal + lateFee.total), status
  }],
  totals: { principal, lateFee, outstanding },
  advance: { balance, ledger[], autoApply },
  suspense: { total, entries[] },
  receipts: [...]   // 해당 계약 입금 원자
}
  • 축→계약 정산 통일 (결함 F, 2026-06-22): 수납 상세 시트는 contract/unit/member 세 리스트 모두에서 열린다(selectedCollecting{axis,key}). 그러나 상세는 본질상 계약 정산 뷰detailForcc = toContract(axis, key)로 대표계약을 구해 periods·연체료(lateFeesFor('contract', cc))·미수·영수증·선수금·조정을 전부 cc 기준으로 조립한다. 이유: 수납은 contract 축에만 collect되므로(useReceipts.applyReceipt) unit/member 행은 collected 빈→outstanding=gross·연체료 과대. cc 라우팅으로 세 축이 동일 정산값을 본다. identity.title만 축 보존(유닛명/멤버명). toContract: member→contracts.memberCode / unit→첫 배분라인 계약 / contract→자기.
  • 축→계약 매핑: 선수금은 계약 단위 귀속. 데모는 CONTRACT_OF(m-hongc-hong 등) 상수. 확정규칙: 실서비스는 lines(세대-계약 관계)로 파생.
  • asOfSheetCollectingDetailArtref(데모 기본 '2025-12-15', selectedCollecting 변경 시 초기화). 수납일 변경 시 이 값이 바뀌어 detailFor가 재파생 → 연체료 replay(§3).

1-1G. 성격×세금 구성요소 모델 (G1 일반화)

G1 변경점: 미수 구성요소를 B서브프로젝트의 "세금만(taxCategory)" 모델에서 성격(nature)×세금 통합 모델로 일반화했다. 레지스트리 src/composables/billingNature.js가 SSOT.

billingNature 레지스트리

js
// src/composables/billingNature.js
export const BILLING_NATURE = {
  관리비: { group: "매출", creditAccountName: "용역매출" },
  수도료: { group: "매출", creditAccountName: "용역매출" },
  장기수선충당금: { group: "부채", creditAccountName: "장기수선충당부채" },
  예비비적립금: { group: "자본", creditAccountName: "예비비적립금" },
};
// 기본 충당 순서 (비용→부채 순)
export const DEFAULT_COMPONENT_ORDER = [
  "과세원금",
  "면세원금",
  "영세원금",
  "장기수선충당금",
  "예비비적립금",
];

export const natureGroup = (nature) => BILLING_NATURE[nature]?.group ?? "매출";
export const componentOf = (charge) =>
  natureGroup(charge?.nature) === "매출" ? `${charge?.taxCategory ?? "과세"}원금` : charge.nature;
export const isVatable = (charge) =>
  natureGroup(charge?.nature) === "매출" && charge?.taxCategory === "과세";
  • componentOf(charge) → 구성요소 키: 매출 성격이면 '${taxCategory}원금'(예: '면세원금'), 부채/자본이면 nature 값 그대로(예: '장기수선충당금'). 미수 구성요소 키의 단일 출처.
  • natureGroup: charge의 성격 그룹('매출' | '부채' | '자본'). 미정의 charge는 '매출' 폴백.
  • isVatable: 과세 VAT 대상(=매출+과세). 분개 VAT 분해 기준.
  • DEFAULT_COMPONENT_ORDER: 충당 기본 순서. 현재 5키. G4에서 프리셋·설정 UI 예정.

구성요소(byComponent) 형태

미수 잔액을 구성요소별로 분해한 맵.

byComponent: {
  과세원금:       8250,   // 매출(과세) — taxCategory='과세' 라인 합산 잔여
  면세원금:       3750,   // 매출(면세) — taxCategory='면세' 라인 합산 잔여
  // 영세원금:    0      → 0이면 생략
  장기수선충당금:  3000    // 부채 — nature='장기수선충당금' 라인 합산 잔여
}

: componentOf(charge)가 반환하는 값. 현재 정의된 키 = 과세원금·면세원금·영세원금·장기수선충당금·예비비적립금·연체료.

currentPeriodByComponent (useAllocations)

useAllocations.invoicingByContract() 등이 반환하는 currentPeriodByComponent는 당기 부과 라인을 구성요소별로 합산한 맵. Σ === currentPeriod.

  • 산출 함수: lineTotalsByComponent() (useAllocations 내부) → sumByComponent(lines, byComp).
  • 동작: 각 charge 라인에 componentOf(charge) 적용 → 구성요소 키별 합산.
  • G1 데모 예시 (홍길동 c-hong 당월 20,000):
    • 일반관리비(과세, 11,000) + 수도료(면세, 5,000) + 장기수선충당금(부채, 4,000) = 20,000
    • currentPeriodByComponent = { 과세원금: 11000, 면세원금: 5000, 장기수선충당금: 4000 }
  • currentPeriodByTax(B 호환 뷰)는 매출 부분집합만 → { 과세: 11000, 면세: 5000 }.

deriveByComponent (useCollectingAllocation)

useForwardingperiod.byComponent를 파생하는 순수 함수.

js
// src/composables/useCollectingAllocation.js
deriveByComponent(billedByComponent, reduced);
  • 입력: billedByComponent(청구시 구성요소 맵) · reduced(= 해당 차수 collected + adjusted).
  • 동작: DEFAULT_COMPONENT_ORDER 순서로 greedy 차감. 각 구성요소에서 min(billed[k], remaining)을 차감.
  • 반환: 잔여 구성요소 맵 (Σ === outstanding).
  • B 호환: principalByTaxbyComponent에서 파생 뷰({ 과세: bc.과세원금, 면세: bc.면세원금, 영세: bc.영세원금 }).

불변식

불변식내용
Σ byComponent === outstanding전 차수·전 구성요소 합계 = 잔여 미수. forwarding period.outstanding과 항상 일치.
Σ currentPeriodByComponent === currentPeriod부과 당기 소계 합 = 당기 부과 총액.
principalByTaxbyComponentprincipalByTax는 byComponent의 매출(과세·면세·영세) 부분집합. 독립 파생(drift 없음).
총 미수 35,000 불변G1 데모에서 수도료(면세)+장기수선충당금(부채) 추가로 성격이 바뀌어도 홍길동 총 receivable은 35,000 유지.

G2~G4 로드맵

서브프로젝트내용상태
G2[수납 처리] 패널 분배충당 결과 행 — 구성요소별 충당액 표시❌ 예정
G3분개 성격별 대변계정 분개 + 과세/면세/영세 VAT 분해 (현재 과세 가정 /1.1)❌ 예정
G4componentOrder 프리셋·설정 UI❌ 예정
공통면세/영세 매출 CoA 실코드·장기수선충당부채 계정코드 (현재 표시명만)❌ 예정

1-1. 원금 taxCategory 분해 (A=소계 배관, B=충당 세구분 정밀)

charge엔 taxCategory('과세'|'면세'|'영세')가 있다. A는 분해를 추가 필드로 흘리고, B는 충당을 세구분으로 정밀화한다(합계·캐논 collect/recordReceipt 무변경).

  • A useAllocations.invoicingBy*()currentPeriodByTax: {과세,면세,영세}(charge.taxCategory별 라인 합산, lineTotalsByTax, Σ === currentPeriod). useForwarding.PRIOR_PERIODS[].billedByTax(이월 시드). useCollectingDetailperiod.principalByTax 통과. AllocationStatusOrg 차수 ▸ 펼침 표시.
  • B (충당 세구분 정밀 — 유도 방식):
    • principalByTax 산출을 A의 비례 근사에서 derivePrincipalByTax(billedByTax, reduced) (순수, useCollectingAllocation)로 교체. reduced(= 그 차수 collected + 원금조정)를 과세→면세→영세 고정 순서 greedy 차감 → 부분수납에서도 정밀(Σ === outstanding). forwarding collected/collect/recordReceipt 무변경(유도만).
    • useCollectingAllocation: componentOrder 배열 양립(['과세원금','면세원금','영세원금','연체료']). componentsOf/dueOf('과세원금'→principalByTax.과세)/allocate/orderedTargets/allocateToComponent가 4구분 + 레거시 문자열 모두 지원. CollectingActionOrg가 토글(원금우선/연체료우선)→배열 매핑, 충당 결과 표가 차수×구분 행.
    • 연체료 라우팅 정정: lateFeeWaiver는 forwarding 원금 미차감(연체료는 forwarding 미추적·엔진 표시) — recordAdjustment/cancelAdjustment가 kind==='lateFeeWaiver'면 adjust/unadjust 생략(원장·전표는 기록). discount/writeOff는 원금 차감 유지.
    • 불변식: Σ_t principalByTax === period.outstanding. 충당현황(AllocationStatusOrg) ↔ 패널(CollectingActionOrg) 세구분 일치.
  • 고급(후속): 저장형 per-구분 원자(collect/recordReceipt per-구분) · 연체료 정식 충당 원자 · per-구분 끝수정책 · componentOrder 커스텀 UI · 다축 fan-out. 분개 VAT taxCategory별=C, 증빙=D. 정본: …/specs/2026-06-20-principal-taxcategory-plumbing-design.md(A) · …/2026-06-20-principal-allocation-by-tax-design.md(B).

2. 충당 알고리즘 — allocate(periods, amount, policy) (순수)

src/composables/useCollectingAllocation.js. 순수 함수(부수효과 없음). 축2개:

  • 축1 periodOrder: 'FIFO'(오래된 차수부터) · 'LIFO'(최근부터) · 'currentThenFIFO'(당기 먼저 → 이월 과거부터). (currentThenLIFO 없음.)
  • 축2 componentOrder: '원금우선'(차수 내 principal→lateFee) · '연체료우선'(lateFee→principal).
  • 차수 정렬은 label 문자열 사전식(localeCompare). currentThenFIFOkind==='current'를 앞에, 나머지 forwarded를 오래된 순으로.

✅ W3 가드 완료(2026-07-06, 배분가드 W3-3·FB-10): allocate 자체는 항상 정렬 소비였으나, 호출 이전 단계 useForwarding.js 계약축 buildRow가 이월 차수(periods)를 배열 삽입순 그대로 넘겨(유닛/멤버축 aggregateAxisRowlabel 정렬) FIFO가 실제로는 "삽입순"으로 동작했다 — 과거 차수를 나중에 추가하면(기초데이터 편집·CSV 등) 최고령이 방치되고 연체료가 계속 발생. buildRow에서 periods 완성 직후 label(YYYY-MM) 오름차순 정렬을 추가(유닛/멤버축과 동일 비교식 재사용). kind==='current'는 라벨이 항상 최신이라 정렬 후에도 배열 끝 위치 유지.

반환

{
  lines: [{ period, component('principal'|'lateFee'), due, allocated }],  // allocated>0만
  appliedPrincipal,   // 원금 충당 합
  appliedLateFee,     // 연체료 충당 합
  excess              // 충당 후 남은 금액(= 선수금 후보)
}

같은 모듈에 orderedTargets(periods, policy) → [{period, component, due}](순수, due>0만, 금액 무관) 보조 export. UI 충당 결과 테이블이 수납 금액 입력 전에도 대상액을 노출하고 직접 입력을 받도록 하는 투영 — 정렬(periodOrder×componentOrder)은 allocate과 동일 헬퍼 공유.

불변식

  • appliedPrincipal + appliedLateFee + excess === amount (항상).
  • allocated <= due (라인별), appliedTotal <= amount.
  • 정책 기본값은 lateFeePolicy.collectionPolicy({ periodOrder:'FIFO', componentOrder:'원금우선' }).
  • 확정규칙(범위 밖): 원금 과세/면세/영세 다중 분해는 미구현 — 현재 원금은 단일 컴포넌트. 다중 taxCategory 충당 순서(과세→면세→영세→연체료)는 후속.

수동(직접 입력)

UI는 자동 결과를 스냅샷해 라인별 allocated를 편집. 확정규칙: 수동 합계 ≤ 수납액, 라인 allocated ≤ due 검증을 BE에서 재확인(불일치 시 확정 차단).


3. 연체료 엔진 계약 (백데이트 replay)

  • 연체료는 interestEngine.computeLateFees(installments, asOf, policy) 산출. 구간분절 단리·declining-balance·양편/한편·상한·반올림(정본: late-fee spec §4·§A).
  • 백데이트 = asOf replay: 수납일을 앞당기면 asOf가 줄어 연체일수↓ → 연체료↓. UI는 수납일을 effective date로 emit('asOf')하고 상위가 detailFor 재호출.
  • 가드(확정규칙): ① 수납일 < 당기 청구확정일 → 차단. ② 수납일 ≤ 마감 회계기간 말일 → 차단(분개 인식일 정책). 프로토는 mock 상수(billingConfirmedDate=2025-12-01, closedPeriodEnd=2025-09-30)로 alert+확정 disable.
  • 확정 충당 시 연체료: useReceipts.recordReceipt원금만 충당(캐논 무편집). 연체료 충당분은 모델 표시(엔진 산출) 기준 — 별도 GL 처리는 PROCESS-RECEIPTS-CANON 연체료 인식 개정(후속 doc)에서 확정.

4. 감면 / 대손 / 인식기준 분개 규칙

확정 액션 payload(emit 'confirm'):

{ amount, date, method,
  discount,        // 원금 감면(에누리) 금액
  lateFeeWaiver,   // 연체료 감면/면제 금액
  writeOff(bool), writeOffAmount,  // 잔여 미수 대손
  reason }         // 감면·대손 사유(확정규칙: 필수)

분개는 JournalPreviewMol(시트 미리보기)과 useBillingJournal(E-series GL 전표)이 인식기준으로 분기(recognitionBasis: '발생'|'현금', lateFeePolicy). 둘은 동일 규칙으로 정합(2026-06-20 #2) — 미리보기 ≠ GL 드리프트 금지.

연체료 수익 인식 시점 — BLUEPRINT E3/E4 정본

연체료수익(0922 영업외수익)을 언제 인식하느냐가 인식기준의 본질.

기준연체료 발생(이월확정) E3수납(충당) E4
발생주의(기본·글로벌·세금계산서 발행처)DR 미수관리비 / CR 연체료수입(0922)useBillingJournal.vouchersLateFeeAccrual. 계약별 useLateFee.lateFeesFor(이월 outstanding × interestEngine, asOf=전기일)DR 보통예금 / CR 미수관리비(원금+연체료) — 연체료도 이미 미수 → lump 회수. 0922 재인식 금지(이중계상 방지)
현금주의(간이 운영)없음(미방출)DR 보통예금 / CR 미수관리비(원금) + CR 연체료수입(0922) split — 수납 시점 인식

⚠️ 핵심: 발생주의에서 E4에 0922를 넣으면 E3와 이중계상된다. 발생주의 0922 인식은 오직 E3. JournalPreviewMol 발생주의도 연체료분을 미수관리비(연체료분)으로 표기(수익 재인식 아님).

발생주의(발생) — 수납/조정 분개

이벤트차변대변
현금 수취보통예금 amount미수관리비 appliedPrincipal · 미수관리비(연체료분) appliedLateFee · 선수금 excess
대손대손충당금 writeOffAmount미수관리비 writeOffAmount
원금 감면(에누리)매출에누리 round(discount/1.1) · 부가세예수금 discount - 공급가미수관리비 discount
연체료 감면연체료수입(차감, 0922) lateFeeWaiver미수관리비 lateFeeWaiver
  • VAT 분해는 과세분 가정(/1.1). 확정규칙: 면세/영세 라인은 부가세 0 — 원금 taxCategory별 분해 필요(현재 단일·과세 가정). 타깃이 과세/면세 혼재이므로 BE는 라인별 세구분으로 매출에누리/부가세 분해.
  • 감면(에누리)·연체료 감면은 매출 차감 — 과세분이면 수정세금계산서 발행 대상.

현금주의(현금)

  • 보통예금 amount / 관리비수입 appliedPrincipal · 연체료수입(0922) appliedLateFee · 선수금 excess.
  • E3 미방출 + 감면·대손 분개 없음(회수액 기준 인식). 미수 자체를 인식하지 않으므로 상계 대상이 없다.

인식기준 가드 — 세금계산서 발행 = 발생주의 강제

  • 과세 매출(세금계산서 발행)이 있으면 현금주의 선택 불가 — 부가세법 공급시기(청구·E2) 기준 매출 인식. 현금주의는 면세·무계산서 운영에 한정.
  • useRecognitionBasisGuard: issuesTaxInvoice(활성 빌링 과세원금>0) · cashAllowed(=!issuesTaxInvoice) · violatesGuard(현금+과세 위반). 연체료 할증 설정(SheetUpdateAccountInfoArt)에서 과세 매출 시 현금 옵션 비활성 + 경고 + 커밋 차단. 정본: PROCESS-RECEIPTS-CANON A-4.

수납 split 데이터 (useReceipts)

  • recordReceipt({ ..., appliedLateFee }) — 연체료 충당분을 receipt에 캡처(appliedLateFee, 기본 0). applyReceiptamount − appliedLateFee를 원금 미수충당(appliedPrincipal) 대상으로 forwarding에 넘긴다. 현금주의 E4 split·발생주의 미수 환입 양쪽의 소스.

불변식

  • 차변 합계 === 대변 합계(모든 모드·E3 포함). UI가 balanced 상태를 표시·검증.
  • 발생주의 E4: amount === appliedPrincipal + appliedLateFee + excess → 보통예금 차변 = 대변 합. 연체료분은 미수관리비로 합산 환입.
  • 현금주의 E4: 동일 식, 단 연체료분은 0922(연체료수입)로 분리 대변.
  • E3 발생: DR 미수 === CR 0922(계약별 연체료 누계).

5. 게이팅 (회계 모듈)

  • useModuleSubscription.accountingEnabled(localStorage 영속). 분개 미리보기·전기 요소accountingEnabled 시 노출.
  • 수납 코어(미수·선수금·가수금·충당·연체료)는 항상 동작 — 모듈과 무관. (정본: accounting-module-gating.)
  • 분개·전표 표시 자체는 외부 회계 SW 병용 대비로 게이팅하지 않는 게 원칙이나, 본 시트의 JournalPreviewMolv-if accountingEnabled로 묶음(상세 패널 한정 — 시연 단순화). 목록의 전기 컬럼/버튼 게이팅과 구분.

6. 확정(Confirm) 경로

  • SheetCollectingDetailArt onActionConfirm(p) 순서 = 현금 → 감면 → 면제 → 대손:
    • p.amount>0useReceipts.recordReceipt({ contractCode, memberCode, amount, method, receivedAt, identified:true })(Method B: ① 미수 직접충당 FIFO useForwarding.collect → ② 잔여 선수금 0259, 미식별이면 가수금 0257).
    • p.discount>0recordAdjustment(kind:'discount', breakdown=allocateToComponent(detail.periods,discount,'principal',policy)).
    • p.lateFeeWaiver>0recordAdjustment(kind:'lateFeeWaiver', …'lateFee'…).
    • p.writeOff && writeOffAmount>0recordAdjustment(kind:'writeOff', …'all'…).
    • 각 단계는 직전 단계 후의 잔여 미수 기준으로 배분한다(detail.value.periods를 단계마다 fresh 접근 — Vue computed 재파생). policy = {periodOrder, componentOrder}는 confirm payload로 전달.

6-1. 조정 캐논 (감면/면제/대손) — useReceivableAdjustments

as-built(이전 한계 해소): 과거엔 discount/lateFeeWaiver/writeOff가 표시·미리보기에만 있고 미수 미반영이었으나, 이제 확정 시 실제 미수를 차감한다.

  • 비현금 미수 차감: useForwarding.adjust(axis, key, breakdown[{period,amount}]) / unadjust. outstanding = max(0, billed − collected − adjusted). collected(현금)와 adjusted(비현금)는 분리 — 충당현황 "수납" 컬럼 의미 보존(불변식).

    다축 fan-out(실구현 주의): 프로토는 조정을 항상 'contract' 축에만 기록한다(adjust('contract', cc, …)). 상세도 detail.adjustments=adjustmentsByContract(cc)로 계약 기준 표시라 데모(단일 계약↔멤버↔유닛)에선 일관. 실서비스는 invoicing처럼 같은 계약을 공유하는 unit/member 축 forwarding 행에도 동일 차감을 전파해야 한다.

  • 조정 원장 adjustments[]: { id, contractCode, memberCode, kind('writeOff'|'discount'|'lateFeeWaiver'), amount, breakdown, reason, at, createdBy, status }.
    • recordAdjustmentadjust 후 원자 push. status: writeOff=승인대기, 그 외 확정.
    • cancelAdjustmentunadjust(미수 원복) + status 취소. 가역(캐논 §F — 역분개 의미).
    • approveAdjustment승인대기승인됨 (전자결재 실연계는 스텁, 후속).
    • detail.adjustments = adjustmentsByContract(cc)(스냅샷; computed 재파생으로 상태변화 반영).
  • 분개(useBillingJournal.vouchersAdjustments, 취소 제외, 차·대 균형):
    kind차변대변
    writeOff대손충당금(0109)미수관리비(0108, 리터럴)
    discount매출에누리(0402) round(amt/1.1) · 부가세예수금(0255) vat미수관리비(0108)
    lateFeeWaiver연체료수입(0922 — 영업외수익, 캐논 A-5 배정 완료)미수관리비(0108)

    불변식/주의: 미수관리비는 GL voucher에서 리터럴 '미수관리비'로 출력(공유 nameOf('0108')는 CORE명 '외상매출금'이라 건드리지 않는다 — E2/E4 회귀 방지). lateFeeWaiver연체료수입(0922) 코드 배정 완료(2026-06-20, useBillingJournal.LATE_FEE_INCOME). 과거 accountCode:'' placeholder 해소.

  • 확정 규칙 미구현(범위 밖): 대손충당금 잔액 추적/부족분 대손상각비(0835) 분기 없음(전액 충당금 상계 가정). 감면 VAT는 과세 가정(/1.1) — 면세/영세 다중 taxCategory 분해 후속. 대손 전자결재 실연계 후속.

7. 엣지케이스

  • 초과 입금: excess>0 → 선수금. 자동충당(autoApply) 켜진 계약은 차월 부과 시 FIFO 상계.
  • 부분 수납: shortfall = reducedDue - applied > 0 → 잔여 미수, 연체 지속(대손 토글 시 0).
  • 감면+대손 동시: reducedDue = totalDue - discount - lateFeeWaiver, 그 후 잔여를 대손. 분개 4종이 모두 balanced.
  • 백데이트로 연체료 0: asOf < 모든 dueDate면 lateFee 0(차수별 0). 충당은 원금만.
  • 미식별 입금: identified:false → 가수금 풀. 재분류(reclassifySuspense)로 계약 매칭 후 미수 충당.
  • 수납취소: 역분개(E7) — 충당 복원·전표 회수. 선수금이 차월 상계된 건은 자동취소 차단(확정규칙).

8. 관련 파일

레이어파일
상세 시트(셸)…/collecting/detail/blocks/SheetCollectingDetailArt.vue (구 detail/IndexView.vue 대체·제거)
진입 선택…/console/overlays/selectedCollecting.js ({axis,key} 공유 ref)
충당(순수)src/composables/useCollectingAllocation.js(allocate·orderedTargets·allocateToComponent)
상세 조립src/composables/useCollectingDetail.js(detail.adjustments 포함)
연체료src/composables/useLateFee.js · interestEngine.js · lateFeePolicy.js
수납 캐논src/composables/useReceipts.js · useForwarding.js(collect/adjust 분리)
조정 캐논src/composables/useReceivableAdjustments.js(대손/감면/면제 원장·가역·승인스텁)
분개src/composables/useBillingJournal.js(E2 청구 / E3 연체료발생 vouchersLateFeeAccrual / E4 수납 + vouchersAdjustments) · JournalPreviewMol.vue(미리보기)
조정 UI…/collecting/detail/blocks/ReceivableAdjustmentsOrg.vue(조정 내역 카드)
게이팅src/composables/useModuleSubscription.js

충당순서 모델 (G2, 2026-06-20)

  • 계층 모델: 축1(연체료 / 원금 / VAT) · 축2(원금 내 매출/비매출) · 축3(매출 내 과세→면세→영세) · 축4(비매출 내 부채→자본).
  • 컴파일러: src/composables/allocationOrder.js compileOrder(preset) → flat 구성요소 배열. 원금 블록 = billingNature.DEFAULT_COMPONENT_ORDER(단일 출처, forwarding deriveByComponent와 공유).
  • 디폴트: 민법 제479조 법정충당(비용→이자→원본) ⇒ 연체료우선. 관리규약이 합의로 달리 정하면(원금우선) 프리셋 교체. 규약 침묵 시 법정.
  • 분류 규칙: allocate().appliedPrincipal = 비연체료 충당(매출원금 + 장기수선충당금 + 예비비적립금), appliedLateFee = 연체료 충당. 불변식 appliedPrincipal + appliedLateFee + excess === amount.
  • 선수금: excessAmount = max(0, 수납액 − Σ outstanding(+lateFee)). 부채/자본 포함 전체 미수 기준 — 부채분 선수금 누수 불가.
  • 불변식: 데모 총미수 35,000 불변. Σ byComponent === outstanding.
  • VAT 축: 부가세 미수 컴포넌트는 현행 byComponent에 없음 → compileOrder VAT 노드 no-op. G3에서 부가세 미수 분리 적재 + 성격별 대변 분개 시 활성.

충당순서 설정 UI 배선 (G4, 2026-06-21)

  • 설정 SSOT: lateFeePolicy 싱글톤 policy.value.collectionPolicy = { periodOrder, componentOrder }. setter setCollectionPolicy(partial) — clone 교체(기본 객체 DEFAULT_LATE_FEE_POLICY 불변), 무효 값(periodOrder∉{FIFO,LIFO,currentThenFIFO}·componentOrder∉{연체료우선,원금우선})은 무시(부분 갱신). useLateFeePolicy()로 노출.
  • 설정 페이지: src/components/service-charge/setting/collecting/MainOrg.vue(/service-charge/setting/collecting). 카드1=축1(연체료우선/원금우선, 법정/규약 배지) · 카드2=차수순서(periodOrder) · 카드3=적용 순서 미리보기(읽기전용 compileOrder 평탄화 + 워크드 예시). 편집 시트 overlays/SheetUpdateCollectingPriority.vue(축1)·SheetUpdateOutstandingPriority.vue(periodOrder)가 setCollectionPolicy로 커밋.
  • 결정/결과 분리(UX 정본): 실제 컨트롤은 축1 + 차수순서 2개만. 축2~4(과세→면세→영세·부채→자본)는 법정 우선순위 부재 → 캐논 디폴트 고정·읽기전용 투명화(거짓 어포던스 제거). 전면 reorder는 향후 비표준 규약용 고급 모드로 분리(YAGNI).
  • 워크드 예시(순수·테스트): allocationOrder.previewLines(componentOrderPreset, periodOrder, amount=PREVIEW_AMOUNT) → 대표 미수 PREVIEW_SAMPLE(72,000)에 allocate 적용한 충당 라인. 연체료우선=연체료 먼저(12,000), 원금우선=원금 먼저·연체료 후순위(부분수납 시 미충당).
  • 수납 패널 반영: CollectingActionOrg가 충당순서 init 소스를 정적 DEFAULT_LATE_FEE_POLICY → reactive useLateFeePolicy().policy.value.collectionPolicy 로 정정(2026-06-21). 시트 오픈마다 현재 설정값으로 prefill(건별 override 유지). ⚠ 과거 인식기준과 동일한 "상수 읽어 무반영" 결함을 충당순서에서도 해소.
  • 불변식 유지: allocate/orderedTargets 인터페이스 무변경(배열 componentOrder 소비). 분개(flat 합계) 미접촉. 데모 충당 결과 불변.
  • 삭제: 부과액 내부순서 reorder 시트(SheetUpdateBillingAmountPriority.vue) — 법정 근거 없는 거짓 어포던스 → 제거.
  • 라벨 SSOT: 구성요소 표시 라벨 billingNature.componentLabel/COMPONENT_LABELS 단일 출처 — 수납 패널·충당순서 설정·연체료 드릴다운 공유(CollectingActionOrg 로컬 맵 폐기).
  • 잔여 G4(후속): compileOrder 구조화 프리셋(객체) 입력 확장 — 단지/규약별 축2~4 커스텀(비표준 규약 대비). VAT 축은 G3에서 활성.

부가세 미수 모델 (G3a, 2026-06-20)

  • 부가세 = 청구·미수 관심사useForwarding.buildRow에서만 주입(invoicing/세금계산서 무편집, 공급가액 유지).
  • billingNature.withVat(byComponent): 부가세 = round(과세원금 × VAT_RATE)(VAT_RATE=0.1), 단일 출처. prior 시드는 공급가액(VAT 파생).
  • DEFAULT_COMPONENT_ORDER에 부가세를 과세원금 직후 삽입 → compileOrder/충당순서 VAT 활성.
  • forwarding period.billed/current/total/outstanding = 공급대가. 불변식 Σ period.byComponent === outstanding.
  • principalByTax/invoicing currentPeriodByTax공급가액(부가세 제외) — 세금계산서 공급가액 정합.
  • 데모(A안 단지 규모 재시드, 2026-06-21): 시드 = demoSeed.js(300세대·월 15만). 홍길동/김철수는 명명예시(앞 2개, 당기는 공유 풀이라 스케일 — 구 36,625는 byte-equivalent 모드(householdCount=2)에서만). 테스트는 매직넘버 대신 __tests__/_seedExpect.js 도메인 파생(forwardingTotal·contractReceivable·totalSales 등)이라 스케일 무관. 명명예시 이월(2025-10/11 과세4000+면세6000)만 고정 리터럴. 정본: docs/handoff/backend/demo-seed-factory.md.
    • 사용자 매뉴얼(manual/collecting-cases* 등)의 예시 숫자(36,625·15,825·10,400)·스크린샷은 미갱신 stale — 명명예시 당기값이 단지 규모로 스케일됨. 스크린샷 재생성과 함께 일괄 리프레시 필요(후속).
  • 연체료 기준 = 공급대가: useLateFeep.outstanding(=공급대가)을 연체료 base로 사용 → 과세분 VAT도 연체료 가산 대상(예: 홍길동 이월 차수 연체료 36→38, 총 48→50). 규약상 VAT 제외가 필요하면 G3b/G4에서 base를 outstanding − 부가세로 설정.
  • G3b: E2 분개 DR 미수(공급대가)/CR 성격별 매출/CR 부가세예수금(과세분) + CoA 실코드. 현재 E2 flat은 미접촉.

도출근거 시트 정합 (2026-06-21)

  • 수납상세·연체료 시트의 원금 도출근거 = E2 청구 분개의 표현(useDerivation.periodPrincipalDerivation → 차변 미수 / 대변 period.byComponent를 계정 매핑). 불변식: 도출근거 대변 합 = 차변 미수 = period.outstanding(공급대가). byComponent 키→계정 매핑(과세원금·면세원금→용역매출 / 부가세→부가세예수금 / 장기수선충당금→장기수선충당부채)이 실제 useBillingJournal E2 대변 구성과 어긋나면 도출근거가 거짓이 됨 → 둘을 같은 매핑 소스로 유지(현재 표시 라벨 useDerivation.CREDIT_ACCOUNT, 실분개 useBillingPreset.componentCredit; 전파 시 단일화 검토).
  • 연체료 도출근거 = 이자엔진 분절(period.lateFee.breakdown 구간·요율·일수) + 결과분개(DR 미수 / CR 연체료수입 0922) — useBillingJournal.vouchersLateFeeAccrual(E3)과 동일 계정. 표시는 Math.round(소수 절사).
  • 정본 패턴: docs/handoff/frontend/derivation-nested-sheet-pattern.md · leysys-design PATTERN-DERIVATION-NESTED-SHEET-2026-06-21.md.

시간축·납기·충당 모델 갱신 (2026-06-21 라이브 리뷰)

  • 차수 = 부과된 월 단위 행. 연체료는 각 차수 납기일 기준 날짜계산(asOf). 부과 중단/공백/종료와 무관하게 미납 차수는 계속 가산.
  • 납기 = 익월 dueDay(기본 5). 시드 period.dueDay차수별 변동(예 5일→6일). useForwarding.nextMonthDay(label, day). 각 차수 연체료는 그 차수 납기로 계산(이월·정책변경 반영).
  • 부과종료/일시중단(당기 없음): useForwardinginvoicingByContract(당기 부과 계약) ∪ PRIOR_PERIODS 키(이월만 있는 계약)로 행 생성. buildRow({hasCurrent:false})면 당기 차수 생략. 3축(contract/unit/member) 대칭 확장. detail.identity.billingStatus=정상/정산대기(당기 부과 유무). 데모: c-end(정산대기·장기연체 3단계), c-gap(중단·재개·차수 공백).
  • 부분납부 연체료 정합: useLateFee가 영수증(useReceipts.receipts[].appliedBreakdown 일자별 원금충당)을 interestEngine payments로 전달 → 납부일 기준 구간 분할(구간별 대상원금 변동). principal = 부과액(공급대가) − 감면. (계약 축만 payments; unit/member 축은 미충당.)
  • E3 정합: vouchersLateFeeAccrualinvoicingByContractforwardingByContract 기반으로 변경(부과종료 정산대기 연체료도 GL E3 반영). totalLateFee(=BS 자산 E3분)와 정합.
  • 충당 내역: detailFor period에 payments(차수별 영수증 원금충당 일자) 추가. 원금 도출근거에 표시. 충당순서 = 법정(민법 §479 연체료→원금) 안내(실제 충당은 수납처리 패널 componentOrder).

충당순서 — 원금 내부 버킷 순서 드래그 설정 (2026-06-22, G4 확장)

오너 요청: 원금 내부 충당 순서를 단지 정책으로 드래그 재정렬. 기존 G4는 "법정 우선순위 없음 → 고정"으로 두었으나, 단지가 어느 버킷부터 충당할지(예: 적립금 보호 위해 부채 먼저)는 실질 정책이므로 설정 가능화.

  • 버킷 모델(billingNature): PRINCIPAL_BUCKETS = ['과세원금','부가세','면세원금','영세원금','부채','자본']. 비매출은 특정 항목(장충금/예비비)이 아닌 분류(부채/자본) 로 버킷화 → 범용(새 부과항목도 group으로 자동 귀속). expandPrincipalOrder(buckets) = 버킷 순서 → 구체 구성요소 키(부채→그룹 등록 성격 전부, 매출 1:1). 기본 확장 ≡ DEFAULT_COMPONENT_ORDER(byte-equivalent).
  • 정책(lateFeePolicy): collectionPolicy.principalOrder(버킷 배열) 추가. setCollectionPolicy({principalOrder})PRINCIPAL_BUCKETS의 순열만 수락(누락·중복·길이불일치 무시), clone 교체로 기본 객체 불변.
  • 엔진 반영(실효):
    • allocationOrder.compileOrder(preset, principalBuckets?)·previewLines(..., principalBuckets?) — 미지정=기본순. 적용순서/워크드 예시가 드래그 순서 반영.
    • useCollectingAllocation.deriveByComponent(billed, reduced, componentOrder?) — 세구분 충당 귀속 순서 주입(미지정=기본순).
    • useForwardingexpandPrincipalOrder(policy.collectionPolicy.principalOrder)를 deriveByComponent에 주입 → 수납 상세 세구분 귀속이 정책 순서 따름(reactive: 설정 변경 시 재계산).
    • CollectingActionOrg(수납 패널)도 compileOrder(componentOrder, principalOrder) → 실제 현금 충당이 버킷 순서 따름.
  • 불변식: 상위 축(연체료↔원금, 민법 §479)은 단계1 componentOrder가 결정 — principalOrder는 원금 내부에만 적용(법정 구간 불침범). 기본 principalOrder 변경 시 전 기존 테스트 무회귀(기본 확장이 DEFAULT_COMPONENT_ORDER와 동치).
  • 테스트: billingNature.spec(PRINCIPAL_BUCKETS·expandPrincipalOrder·bucketLabel) · lateFeePolicy.spec(순열 검증) · allocationOrder.spec(버킷 주입) · useCollectingAllocation.spec(deriveByComponent order).

설정 스코프 — 프리셋/글로벌/로컬 3계층 (2026-06-22)

정본 ADR: docs/decisions/SETTINGS-SCOPE-GLOBAL-LOCAL-PRESET-2026-06-22.md. 충당순서(collectionPolicy)를 시범 적용(전부 frozen 클래스).

  • 계층: 프리셋(PRESET_COLLECTION_POLICY, 불변·되돌리기 종착) → 글로벌(globalCollectionPolicy ref, 신규 콘솔 기본값, 초기=프리셋 복사) → 로컬(policy.collectionPolicy, 이 콘솔·빌링 구동).
  • frozen 보장(규칙3): 글로벌 변경이 기존 콘솔(로컬) 불변. 두 ref가 독립 — 라이브 바인딩 없음. 콘솔 생성 시 글로벌을 통째 스냅샷 복사(프로토는 단일 콘솔 → 로컬 단일 ref). 단위 테스트 lateFeePolicy.spec의 "규칙3" 단언.
  • 엔진 무영향: 빌링 엔진(useForwarding·CollectingActionOrg)은 항상 로컬(policy.value.collectionPolicy)만 읽음 → 글로벌 추가는 additive(전기·집계 무변경).
  • 세터: setCollectionPolicy(partial, scope='local'|'global') — 스코프별 라우팅, 검증 공통(applyCollectionPolicy). resetCollectionPolicy(scope) — 로컬→글로벌 / 글로벌→프리셋. 모두 clone 교체(프리셋·기본 객체 불변).
  • 전파 정책 클래스(설정별 태깅): frozen(기본·스냅샷)/live(글로벌만·로컬금지: 세금구분·성격·CoA)/effective-dated(거래일 버전: 부가세·법정요율)/notify(opt-in). "같이 바뀌어야 하는 값"은 force-sync가 아니라 live/effective-dated로 모델링 — frozen 동결 보장과 비충돌. (충당순서는 frozen 단일 클래스라 가장 단순한 파일럿.)
  • 후속: live/effective-dated 실제 적용, 멀티콘솔 consoleConfig[id] 키 스토어, notify 배너.

연체요율 설정 ↔ 엔진 배선 (2026-06-22)

연체료 할증 설정 화면의 연체요율 표가 하드코딩 목업(13개월 12/24/36/48%)이고 엔진(interestEngine.rateSchedule 0/3/6개월=3/4/5%)과 무연결이던 것 → 배선.

  • lateFeePolicy.setRateSchedule(schedule): tier 배열 [{fromMonths, annualRate}] 정규화(0개월 구간 필수·정렬·중복제거)·clone 교체(DEFAULT 불변). rateScheduleRows(schedule): 구간 라벨·연%·월% 파생(표시/편집 공유, pure).
  • 설정 화면(setting/surcharge)이 policy.rateSchedule를 표시·편집 → interestEngine.lateFeeOf가 즉시 그 요율로 계산(useLateFee→forwarding→수납 상세·E3 GL 연쇄 반영). 표시값 "일할 단리·한편넣기"도 dayCount에서 파생(과거 "월할·양편넣기" 목업 정정 — 엔진은 일할 단리·inclusion '한편').
  • 불변식: 첫 구간 fromMonths=0(납기 직후) 필수. rateSegments가 fromMonths 경계로 일수 분절 → 단계 이율. 기존 데모 기대값(3/4/5%)은 DEFAULT 유지라 무회귀.
  • 스코프: rateSchedule은 effective-dated 클래스 후보(법정 요율). 현재 단일 정책값(로컬) — 거래일 기준 버전화는 ADR SETTINGS-SCOPE §후속.

리스트 페이지네이션 — API 계약(후속, 2026-06-22)

수납 목록(계약·유닛·멤버 3축)은 프로토에서 클라이언트 사이드 슬라이싱으로 페이지네이션한다(전체 행을 메모리에 빌드 후 usePagination.slice로 자름). 기본 페이지 크기 20, 옵션 [20/50/100/전체].

  • 현재(프로토): DynamicTableOrgcollectings(전체) computed 불변 → 합계·일괄선택은 전 페이지 기준. slice는 표시 전용.
  • 실서비스 전환: 목록 조회 API에 page(1-base)·size(또는 all) 파라미터 + 응답에 total(전체 건수) 추가. FE는 setTotal(total)만 서버값으로 바꾸고 slice는 이미 페이지된 응답이면 no-op. 정렬·검색 파라미터와 함께 서버에서 처리(필터 후 1페이지 리셋은 FE setTotal 클램프 또는 명시 setPage(1)).
  • 합계의 의미: 페이지 합계가 아니라 전체(필터 적용) 합계가 필요 → 목록 API와 별개로 집계 엔드포인트(또는 응답 메타 aggregates)로 전 범위 합계 제공. 화면 하단 "받을금액" 등 총계는 페이지가 아닌 전체 기준.
  • FE 정본: docs/handoff/frontend/list-pagination.md.

부과액 vs 청구액 — 정본 체인 (2026-06-22)

용어 혼용(glossary "부과(청구)한 금액" · 이관 "청구금액"=gross · 고지서 "청구액"=net) 정리. 정본:

부과액(levy 총액 = 매출분 + 비매출분[장충금·예비비 등])   ← "매출"과 동의어 아님(세구분 매출/비매출로 분리)
  + 이월 미납 + 연체료
  = 받을금액(총 미수)                    ← useCollectingDetail.totals.outstanding (= principal + lateFee)
  − 선수금 잔액 적용                       ← useReceipts.advanceBalanceByContract (과오납·선납 원장)
  = 청구액(납부하실 금액)                   ← totals.payable = max(0, outstanding − advanceBalance)
  • 부과액 ≠ 매출: 부과액은 levy 총액. 매출은 그 중 매출분류분(componentGroup '매출'). 비매출(장충금=부채 등) 포함.
  • 청구액: totals.payable(useCollectingDetail) — 수납 상세 상단 스트립 단일 강조값. 선수금 0이면 = 받을금액.
  • 선수금은 실재 원장(목업 아님): 과오납 자동적립(applyReceipt 잔여→creditAdvance)·가수금 재분류·환급(advance-receipt 콘솔 탭). 단 받을금액엔 미차감 → 청구액에서만 차감(표시 netting).
  • glossary 정정: 부과액 정의에서 "(청구)" 제거 + 청구액 신설. 이관 "청구금액"→"부과금액"(gross 의미 정렬).
  • 후속: 고지서(InvoiceTable/Preview) 정적 목업 → payable 파생 실배선(부과합계 − 선수금 = 청구액 · 납기후 청구액).

2026-06-26 — 3차 감사 수정 (T4·T5)

  • T4 정정수납 연체료 승계 (useCollectingActions.onCancelConfirm · SheetReadBillingArt.onCancelConfirm): 수납취소 후 정정수납 recordReceipt가 원수납 appliedLateFee를 승계(Math.min(원.appliedLateFee, 정정금액)). 누락 시 정정수납이 전액 원금현금 처리 → 현금주의 0922 연체료수입 라인 누락·발생주의 차수 과충당. 테스트: useCollectingActions.spec.js(승계·clamp).
  • T5 suspense asOf 필터 (useReceipts.suspenseBalance(asOf) · useCollectingDetail.detailFor): suspenseBalanceasOf 인자 시 receivedAt ≤ asOf 미식별입금만 합산(무인자=전체). detailForasOf 전달 → 백데이트 스냅샷에서 미래 가수금 노출 차단(as-of 순수성 불변식). 테스트: useReceipts.spec.js.

2026-07-06 — 정책 게이트 W4 (FB-6·FB-7·FB-12) — 소급 재작성 차단

Fable 1차 감사(docs/AUDIT-CORE-BILLING-FABLE-1ST-2026-07-02.md) FB-6/FB-7/FB-12 해소. 공통 뿌리: E-series 전표는 저장 아닌 파생(derive-on-read) — 정책 입력(요율·발생중지·인식기준·연체료 asOf)이 바뀌면 이미 전기완료(useGeneralLedger.postedIds)·마감(useClosing.closedPeriods) 전표의 내용이 소급 변경되나 상태('전기완료')는 그대로라 원장 무결성이 붕괴하던 문제. 설계: docs/superpowers/specs/2026-07-06-policy-posting-gate-design.md. 판정근거: .superpowers/sdd/task-1-report.md(T1 회계 타당성).

T1(FB-12) — E3·E4 연체료 asOf 정합 (useBillingJournal.vouchersCollection)

appliedLateFee의 asOf(수납확정 시점 useCollectingActionsasOf.value, 기본 REFERENCE_DATE)와 E3 발생 asOf(POSTING_DATE, 전기일)가 3일 다르다. 연체료는 asOf에 단조비감소하므로 정상 흐름에서도 appliedLateFee(REFERENCE_DATE 기준)가 E3 발생액(POSTING_DATE 기준)을 넘어설 수 있다(홍길동 실측: 146→152, +6/3일) — 그 초과분을 그대로 0108에 환입하면 "발생 없는 환입"(유령환입)이 된다.

  • 정합 방식(채택) = clamp: vouchersCollection()이 계약별 accruedCap[cc] = lateFeesFor('contract', cc, POSTING_DATE).total을 1회 계산하고, receipts를 순회하며 lateFeeReversed = min(appliedLateFee, cap − 이미클램프소비)만 0108 대변으로 환입. reversedSoFar(계약별 누적, 이 파생 함수 실행 스코프 한정)로 순서 무관하게 계약 총액이 min(Σ요청, cap)으로 수렴(그리디 클램프 표준 성질).
  • 초과분 처리: lateFeeDirectIncome = 현금주의 ? 전액 : (appliedLateFee − lateFeeReversed) — 0922(연체료수입) 라인으로 수납 시점 직접 인식(현금주의 split과 동형). 직접식별(E4)·재분류(E-reclass) 두 분기 모두 동일 조건(lateFeeDirectIncome > 0)으로 일반화.
  • 채택 근거(asOf 고정안 (b) 기각): (a) UI가 "오늘 기준 연체료"를 보여주고 그 금액을 충당하는데 분개만 며칠 전 금액으로 재계산하면 표시(수납 상세 시트)≠분개(GL) 드리프트를 새로 만든다. (b) POSTING_DATE는 매월 갱신되는 상수라 asOf 고정은 매번 "이번 달 이전분만 인정"을 하드코딩해야 해 유지보수 비용이 크다. (c) clamp는 E3/E4 asOf 차이를 그대로 두고 차액만 후단에서 흡수하므로 더 견고. 상세: .superpowers/sdd/task-1-report.md §3.
  • ⚠ 잔여 리스크(비차단, 후속 과제):
    1. clamp 소비 순서는 receipts 배열(기록)순이지 receivedAt 시간순이 아니다 — 계약 합계(Σ환입 = min(Σ요청,cap))는 정확하나, 어느 개별 영수증이 클램프(초과분 0922로 전환)됐는지의 per-voucher 귀속은 시간순과 다를 수 있다. 후속: 클램프 계산 전 receivedAt 정렬.
    2. lateFeeWaiver(연체료 감면 조정, useReceivableAdjustments)는 이번 스코프 밖 — 동일한 asOf-mismatch 과다환입 리스크가 이론상 있으나 미착수.
  • 회귀0: cap 이내(lateFeeDirectIncome=0)인 기존 케이스는 구코드와 동치(신규 로직 미개입). 데모 시드(홍길동 2025-11-15 수납)는 appliedLateFee=0으로 기록돼 clamp 자체가 개입하지 않음 — 데모 초기 화면·기존 GL 스냅샷 byte-identical.

T2(FB-6) — 요율·발생중지 편집 effective-dating + posted/closed 게이트

lateFeePolicy.setRateSchedule(schedule, effectiveFrom)이 effectiveFrom 없이 호출되면 현재 유효 버전(baseline)을 그대로 교체 — E3가 rateScheduleAt(거래일)로 조회하는 과거 차수까지 소급 재산정됐다.

  • 효과일 강제: 편집(effectiveFrom 미지정) 호출은 항상 EDIT_DEFAULT_EFFECTIVE_FROM(= REFERENCE_DATE + 1일, 차기 청구기 시작 — 하드코딩 아닌 파생, REFERENCE_DATE 롤포워드에 자동 동기)부터 새 버전으로 추가한다. 과거·당기 차수(≤REFERENCE_DATE)는 옛 버전을 그대로 유지(E3 소급 불변). 명시 effectiveFrom 호출(기존 테스트)은 회귀 없이 그대로 동작.
  • posted/closed 게이트: setRateSchedule(schedule, effectiveFrom, lockedUntil)lockedUntil(선택, 'YYYY-MM-DD') truthy & target ≤ lockedUntil이면 {ok:false, reason}로 거부(전기완료/마감 기간 이전으로 소급 개정 불가 — 역분개 절차로 정정). setAccrualPause(partial, lockedUntil)는 버전 인프라가 없어(전역 단일 창) lockedUntil truthy 시 편집 자체를 통째 거부(보수적 게이트).
  • 순환 회피(ADR): lateFeePolicy는 leaf(useGeneralLedger/useClosing 미import) 유지. lockedUntil 판정은 소비처(SheetUpdateSurchargeRate.vue/SheetUpdateSurchargePause.vue)가 useGeneralLedger().postedVouchers·useClosing().closedPeriods를 조회해 계산 후 주입(W3b postAllUnposted(gate) 선례와 동형). lockedUntil = max(postedVouchers 최신 date, closedPeriods 최신 기간의 월말일).
  • UI: SheetUpdateSurchargeRate.vue 안내문이 EDIT_DEFAULT_EFFECTIVE_FROM 실제 값을 노출("적용예정일 {날짜}부터 반영, 그 이전 차수·전기분은 소급 변경되지 않음")하도록 정정(17ed43b24 리뷰 — 이전엔 "즉시 반영" 정적 문구가 effective-dating 도입 후에도 남아 FALSE 표시).
  • 관련 파일: lateFeePolicy.js:84-130(rateSchedule) · :45-57(accrualPause) · service-charge/setting/surcharge/overlays/SheetUpdateSurchargeRate.vue · SheetUpdateSurchargePause.vue.

T3(FB-7) — 인식기준 전환 전기·마감 게이트

setRecognitionBasis(basis)postedVouchers/closedPeriods 상태와 무관하게 policy.recognitionBasis를 즉시 교체 — E3(발생 시 방출)·E4(환입 방식)가 recognitionBasis를 조회 시점에 파생하므로, 전기·마감 후 전환하면 이미 전기된 E3가 소급으로 사라지거나(발생→현금) 마감기간의 환입 방식이 뒤바뀐다(0922/0108 drift). 기존 useRecognitionBasisGuard(과세매출 존재 시 현금주의 차단)로는 면세 전용 사업장(과세매출 0, cashAllowed=true)에서 이 경로가 막히지 않았다.

  • API: setRecognitionBasis(basis, hasPostedOrClosed) — 2번째 인자(boolean, 소비처 주입) truthy면 {ok:false, reason:'전기·마감 전표 존재 — 역분개/마감취소 후 전환하세요'}로 전환 자체를 통째 거부. recognitionBasis도 버전 인프라가 없는 전역 단일 값(accrualPause와 동형) — 부분 교체 불가라 존재 시 전면 차단.
  • 소비처: SheetUpdateAccountInfoArt.vue(service-charge/setting/surcharge/overlays/)가 hasPostedOrClosed = postedVouchers.value.length > 0 || closedPeriods.size > 0을 조회해 주입(T2 lockedUntil 선례와 동형, 순환 회피).
  • 기존 가드와 관계: useRecognitionBasisGuard(cashAllowed) 가드는 그대로 유지 — 이번 게이트는 그 위에 추가된 독립 게이트(둘 중 하나라도 위반이면 커밋 차단). postedIds가 비어있으면(데모 초기 상태) 전환 허용 — 회귀0.

캐논 개정 요약

  • PROCESS-RECEIPTS-CANON A-6 분개(확정) — E4 발생주의 라인이 min(appliedLateFee, cap잔여) 환입 + max(0,초과분) 0922로 갱신(§ 위 T1).
  • PROCESS-RECEIPTS-CANON A-4 인식기준 가드 — cashAllowed 가드에 posted/closed 게이트가 독립 병존으로 추가(§ 위 T3).
  • 라인 384 "연체요율 설정 ↔ 엔진 배선" 절의 "스코프: rateSchedule은 effective-dated 클래스 후보 … 거래일 기준 버전화는 후속" 문구는 이번 웨이브로 일부 이행 완료(편집 경로의 effective-dating 강제 — 엔진 조회(rateScheduleAt)는 기존 그대로).