다크모드
BE 참고 — 수납 상세 시트 / 수납처리 액션 + 분개
독자: BE 개발자/AI. 수납 상세 시트(
SheetCollectingDetailArt, 진입=collecting 목록 행 클릭 → 공유 refselectedCollecting{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— 원금(outstanding)·dueDate·kind('forwarded'|'current')·billed·collecteduseLateFee.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}). 그러나 상세는 본질상 계약 정산 뷰 —detailFor는cc = 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-hong→c-hong등) 상수. 확정규칙: 실서비스는lines(세대-계약 관계)로 파생. asOf는SheetCollectingDetailArt의ref(데모 기본'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)
useForwarding의 period.byComponent를 파생하는 순수 함수.
js
// src/composables/useCollectingAllocation.js
deriveByComponent(billedByComponent, reduced);- 입력:
billedByComponent(청구시 구성요소 맵) ·reduced(= 해당 차수collected + adjusted). - 동작:
DEFAULT_COMPONENT_ORDER순서로 greedy 차감. 각 구성요소에서min(billed[k], remaining)을 차감. - 반환: 잔여 구성요소 맵 (
Σ === outstanding). - B 호환:
principalByTax는byComponent에서 파생 뷰({ 과세: bc.과세원금, 면세: bc.면세원금, 영세: bc.영세원금 }).
불변식
| 불변식 | 내용 |
|---|---|
Σ byComponent === outstanding | 전 차수·전 구성요소 합계 = 잔여 미수. forwarding period.outstanding과 항상 일치. |
Σ currentPeriodByComponent === currentPeriod | 부과 당기 소계 합 = 당기 부과 총액. |
principalByTax ⊆ byComponent | principalByTax는 byComponent의 매출(과세·면세·영세) 부분집합. 독립 파생(drift 없음). |
| 총 미수 35,000 불변 | G1 데모에서 수도료(면세)+장기수선충당금(부채) 추가로 성격이 바뀌어도 홍길동 총 receivable은 35,000 유지. |
G2~G4 로드맵
| 서브프로젝트 | 내용 | 상태 |
|---|---|---|
| G2 | [수납 처리] 패널 분배충당 결과 행 — 구성요소별 충당액 표시 | ❌ 예정 |
| G3 | 분개 성격별 대변계정 분개 + 과세/면세/영세 VAT 분해 (현재 과세 가정 /1.1) | ❌ 예정 |
| G4 | componentOrder 프리셋·설정 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(이월 시드).useCollectingDetail→period.principalByTax통과.AllocationStatusOrg차수 ▸ 펼침 표시. - B (충당 세구분 정밀 — 유도 방식):
principalByTax산출을 A의 비례 근사에서derivePrincipalByTax(billedByTax, reduced)(순수,useCollectingAllocation)로 교체.reduced(= 그 차수collected + 원금조정)를 과세→면세→영세 고정 순서 greedy 차감 → 부분수납에서도 정밀(Σ === outstanding). forwardingcollected/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).currentThenFIFO는kind==='current'를 앞에, 나머지 forwarded를 오래된 순으로.
✅ W3 가드 완료(2026-07-06, 배분가드 W3-3·FB-10):
allocate자체는 항상 정렬 소비였으나, 호출 이전 단계useForwarding.js계약축buildRow가 이월 차수(periods)를 배열 삽입순 그대로 넘겨(유닛/멤버축aggregateAxisRow만label정렬) 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).applyReceipt는amount − 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 병용 대비로 게이팅하지 않는 게 원칙이나, 본 시트의
JournalPreviewMol은v-if accountingEnabled로 묶음(상세 패널 한정 — 시연 단순화). 목록의 전기 컬럼/버튼 게이팅과 구분.
6. 확정(Confirm) 경로
SheetCollectingDetailArtonActionConfirm(p)순서 = 현금 → 감면 → 면제 → 대손:p.amount>0→useReceipts.recordReceipt({ contractCode, memberCode, amount, method, receivedAt, identified:true })(Method B: ① 미수 직접충당 FIFOuseForwarding.collect→ ② 잔여 선수금 0259, 미식별이면 가수금 0257).p.discount>0→recordAdjustment(kind:'discount', breakdown=allocateToComponent(detail.periods,discount,'principal',policy)).p.lateFeeWaiver>0→recordAdjustment(kind:'lateFeeWaiver', …'lateFee'…).p.writeOff && writeOffAmount>0→recordAdjustment(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 }.recordAdjustment→adjust후 원자 push. status: writeOff=승인대기, 그 외확정.cancelAdjustment→unadjust(미수 원복) + 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.jscompileOrder(preset)→ flat 구성요소 배열. 원금 블록 =billingNature.DEFAULT_COMPONENT_ORDER(단일 출처, forwardingderiveByComponent와 공유). - 디폴트: 민법 제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 }. settersetCollectionPolicy(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→ reactiveuseLateFeePolicy().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/invoicingcurrentPeriodByTax는 공급가액(부가세 제외) — 세금계산서 공급가액 정합.- 데모(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 — 명명예시 당기값이 단지 규모로 스케일됨. 스크린샷 재생성과 함께 일괄 리프레시 필요(후속).
- ⚠ 사용자 매뉴얼(
- 연체료 기준 = 공급대가:
useLateFee가p.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 키→계정 매핑(과세원금·면세원금→용역매출 / 부가세→부가세예수금 / 장기수선충당금→장기수선충당부채)이 실제useBillingJournalE2 대변 구성과 어긋나면 도출근거가 거짓이 됨 → 둘을 같은 매핑 소스로 유지(현재 표시 라벨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-designPATTERN-DERIVATION-NESTED-SHEET-2026-06-21.md.
시간축·납기·충당 모델 갱신 (2026-06-21 라이브 리뷰)
- 차수 = 부과된 월 단위 행. 연체료는 각 차수 납기일 기준 날짜계산(asOf). 부과 중단/공백/종료와 무관하게 미납 차수는 계속 가산.
- 납기 = 익월
dueDay일(기본 5). 시드period.dueDay로 차수별 변동(예 5일→6일).useForwarding.nextMonthDay(label, day). 각 차수 연체료는 그 차수 납기로 계산(이월·정책변경 반영). - 부과종료/일시중단(당기 없음):
useForwarding가invoicingByContract(당기 부과 계약) ∪PRIOR_PERIODS키(이월만 있는 계약)로 행 생성.buildRow({hasCurrent:false})면 당기 차수 생략. 3축(contract/unit/member) 대칭 확장.detail.identity.billingStatus=정상/정산대기(당기 부과 유무). 데모: c-end(정산대기·장기연체 3단계), c-gap(중단·재개·차수 공백). - 부분납부 연체료 정합:
useLateFee가 영수증(useReceipts.receipts[].appliedBreakdown일자별 원금충당)을 interestEnginepayments로 전달 → 납부일 기준 구간 분할(구간별 대상원금 변동). principal = 부과액(공급대가) − 감면. (계약 축만 payments; unit/member 축은 미충당.) - E3 정합:
vouchersLateFeeAccrual이invoicingByContract→forwardingByContract기반으로 변경(부과종료 정산대기 연체료도 GL E3 반영).totalLateFee(=BS 자산 E3분)와 정합. - 충당 내역:
detailForperiod에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?)— 세구분 충당 귀속 순서 주입(미지정=기본순).useForwarding가expandPrincipalOrder(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, 불변·되돌리기 종착) → 글로벌(globalCollectionPolicyref, 신규 콘솔 기본값, 초기=프리셋 복사) → 로컬(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/전체].
- 현재(프로토):
DynamicTableOrg의collectings(전체) computed 불변 → 합계·일괄선택은 전 페이지 기준.slice는 표시 전용. - 실서비스 전환: 목록 조회 API에
page(1-base)·size(또는all) 파라미터 + 응답에total(전체 건수) 추가. FE는setTotal(total)만 서버값으로 바꾸고slice는 이미 페이지된 응답이면 no-op. 정렬·검색 파라미터와 함께 서버에서 처리(필터 후 1페이지 리셋은 FEsetTotal클램프 또는 명시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):suspenseBalance가asOf인자 시receivedAt ≤ asOf미식별입금만 합산(무인자=전체).detailFor가asOf전달 → 백데이트 스냅샷에서 미래 가수금 노출 차단(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(수납확정 시점 useCollectingActions의 asOf.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. - ⚠ 잔여 리스크(비차단, 후속 과제):
- clamp 소비 순서는
receipts배열(기록)순이지receivedAt시간순이 아니다 — 계약 합계(Σ환입 = min(Σ요청,cap))는 정확하나, 어느 개별 영수증이 클램프(초과분 0922로 전환)됐는지의 per-voucher 귀속은 시간순과 다를 수 있다. 후속: 클램프 계산 전receivedAt정렬. lateFeeWaiver(연체료 감면 조정,useReceivableAdjustments)는 이번 스코프 밖 — 동일한 asOf-mismatch 과다환입 리스크가 이론상 있으나 미착수.
- clamp 소비 순서는
- 회귀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)는 버전 인프라가 없어(전역 단일 창)lockedUntiltruthy 시 편집 자체를 통째 거부(보수적 게이트). - 순환 회피(ADR):
lateFeePolicy는 leaf(useGeneralLedger/useClosing 미import) 유지.lockedUntil판정은 소비처(SheetUpdateSurchargeRate.vue/SheetUpdateSurchargePause.vue)가useGeneralLedger().postedVouchers·useClosing().closedPeriods를 조회해 계산 후 주입(W3bpostAllUnposted(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을 조회해 주입(T2lockedUntil선례와 동형, 순환 회피). - 기존 가드와 관계:
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)는 기존 그대로).