다크모드
매입 P1(매입확정)/P2(지급충당) — BE 참고
⚠️ 2026-07-07 갱신: 아래 §4 "초과 차단 가드"는 P3 슬라이스에서 폐기됐다(초과 지급 → 선급금 0131 전환으로 대체). 최신 계약은
docs/handoff/backend/purchasing-p3-p6.md§1·§2-2 참고 — 이 문서는 P1/P2 첫 슬라이스 as-built 기록으로 보존한다(§8 후속 항목도 P3/P6 슬라이스로 상당수 해소됨).골드너스 통합 P-시리즈 첫 슬라이스. 정본 설계:
docs/superpowers/specs/2026-07-07-purchasing-p1-p2-design.md. 정본 블루프린트:docs/BLUEPRINT-PURCHASING-ACCOUNTING-2026-07-05.md(§1 원칙·§2 이벤트·§3 충당·§6 용어·§7 수용테스트 — 이번 슬라이스로 첫 실체화). E-시리즈(청구/수납/이월) 대칭 구조 —docs/handoff/backend/service-charge-metering.md등과 동일한 canon-chain 원칙을 매입 축에 적용한다.
1. 개요
이 슬라이스는 매입(매입확정, P1)과 지급(지급확정·충당, P2)을 회계 보조원장으로 구현한다. E-시리즈의 미러:
useForwarding(이월 미수 원장 + collect() FIFO 충당) ↔ usePurchases(매입 원장 + pay() FIFO 충당)
useReceipts(입금 원자 소유, recordReceipt→collect 호출) ↔ usePayments(지급 원자 소유, recordPayment→pay 호출)
useBillingJournal(E1/E4 전표) ↔ usePurchaseJournal(P1/P2 전표)스코프: P1(매입확정)·P2(지급확정·FIFO충당)·거래처 원장(read-only)·P1/P2 GL 전표(미전기 생성→수동 전기)·/purchasing transaction·payment 콘솔 실화·회계 게이팅. 스코프 아웃: P3(선지급)·P4(카드)·P5(정정/반품)·P6(미식별출금)·3자통과·세금계산서 허브 자동연동·은행수집·3원대사.
2. 데이터 모델
2-1. 매입(purchase, P1) — usePurchases.js
{
id, // 'pur-{seq}' — usePurchases가 부여
vendorCode, // 거래처 식별(명칭 마스터 부재 — 코드 그대로 폴백, §5 참고)
purchaseDate, // 'YYYY-MM-DD' — 전표 일자 기준(발생일)
category, // '용역' | '재화' | '경비' — payableAccountFor() 분기 입력
accountCode, // 사용자 선택 비용/자산 계정(차변) — 예 0831 지급수수료·0167 저장품·0837 건물관리비
supplyAmount, // 공급가액(부가세 제외)
taxType, // '과세' | '면세' | '영세' | '불공제'
vatAmount, // taxType==='과세'일 때만 Math.round(supplyAmount × vatRateAt(purchaseDate)), 그 외 0
payable, // supplyAmount + vatAmount (공급대가 = 미지급 총액)
payableAccountCode, // '0251'(재화) | '0253'(용역·경비 — category!=='재화'는 전부 0253)
documentType, // '세금계산서' | '계산서'
memo
}- 불변식:
payable = supplyAmount + vatAmount.vatAmount는 taxType==='과세'일 때만 0보다 크다. - append-only:
recordPurchase()로만 추가. 전기 후 수정 금지(E-시리즈 원칙 상속). 현재 취소/역분개 상태 없음(P5 정정/반품 후속). - paidByPurchase(모듈 프라이빗 오버레이):
purchaseId → 충당누계.usePayments는 이 맵을 직접 쓰지 않고 반드시usePurchases().pay()/unpay()를 통해서만 변경한다.
2-2. 지급(payment, P2) — usePayments.js
{
id, // 'pay-{seq}'
vendorCode,
paymentDate, // 전표 일자 기준(지급일)
amount, // 요청 지급액(가드 통과분만 기록됨 — 초과분 별도 기록 없음)
method, // '계좌이체' 기본값 — '자동이체'|'현금'|'카드' 등
memo,
applied, // 실제 충당액(정상 케이스는 amount와 동일 — 가드가 초과를 사전 차단하므로)
appliedBreakdown, // [{ purchaseId, amount }] — pay() 전/후 peekPaid(vendorCode) diff로 구성
status // '정상' | '취소'
}3. 캐논 사슬 (Canon Chain)
usePurchases ── 매입 원장 소유. purchases[](append-only) + paidByPurchase{}(충당 오버레이).
├─ payablesByVendor() ── 거래처별 열린 채무(outstanding>0만, purchaseDate 오름차순)
├─ payableBalanceByVendor() ── 거래처별 Σoutstanding
├─ pay(vendorCode, amount) ── FIFO 소진(useForwarding.collect 동형). 무판단 클램프(가드 없음).
├─ peekPaid(vendorCode) ── purchaseId→충당액 스냅샷(diff용)
└─ unpay(vendorCode, breakdown)── 역산 복원(uncollect 동형)
▲
│ (호출)
usePayments ── 지급 원자 소유. payments[].
├─ recordPayment(...) ── 가드(§4) → pay() → before/after diff로 appliedBreakdown 구성 → payments.push
├─ cancelPayment(id) ── idempotent(이미 취소면 noop) → unpay(breakdown) → status='취소'
└─ paidByVendor() ── 참고 집계(비취소 Σapplied, payablesByVendor의 paid와 별도 소스지만 항상 일치해야 함)
│
▼
usePurchaseJournal ── P1/P2 GL 전표 생성(미전기). useGeneralLedger.postedIds 재사용(id 키만).
├─ vouchersPurchase() ── P1: DR accountCode+0135(과세만) / CR payableAccountCode
└─ vouchersPayment() ── P2: DR payableAccountCode(그룹핑) / CR 0103
│
▼
useVendorLedger ── 신규 원자 없음. purchases+payments를 거래처별 시계열 병합(read-only VM).단일 소스 원칙: paidByPurchase는 usePurchases 파일 내부에만 있고, usePayments/useVendorLedger/usePurchaseJournal 어디에서도 직접 쓰지 않는다 — 전부 pay()/unpay()/payablesByVendor()/payableBalanceByVendor() 파생 함수를 통해서만 읽고 쓴다.
4. FIFO 충당 + 초과 차단 (P2 핵심)
usePayments.recordPayment():
- amount>0 가드(콘솔 폼 검증 우회 대비):
!(Number(amount) > 0)→{ ok:false, reason:'지급 금액은 0보다 커야 합니다', applied:0 }, 아무것도 기록 안 함. - 초과 차단 가드(spec §3):
amount > payableBalanceByVendor()의 해당 거래처 balance→{ ok:false, reason:'채무 초과 — 선지급(P3)은 후속 슬라이스', applied:0 }.pay()를 아예 호출하지 않는다 — 부분충당 후 거부(원자성 위반) 금지.usePurchases.pay()자체는 정책 판단 없는 무음 클램프이므로, 가드는 반드시 호출 "전"에 이 파일(usePayments)에서 검사한다. - 가드 통과 시:
peekPaid(vendorCode)로 before 스냅샷 →pay(vendorCode, amount)(FIFO, 오래된purchaseDate우선) →peekPaid(vendorCode)로 after 스냅샷 → diff(after - before, 0보다 큰 것만)로appliedBreakdown구성. payments.push(...),{ ok:true, id, applied, appliedBreakdown }반환.
cancelPayment(id): 이미 status==='취소'면 noop(idempotent). 아니면 unpay(payment.vendorCode, payment.appliedBreakdown)로 채무를 역산 복원한 뒤 status='취소'로 마킹(레코드 자체는 삭제하지 않음 — 감사 추적).
몰아지급(bulk payment): 별도 로직 없음 — FIFO가 자연 처리한다. amount가 k×월정액이어도 pay()는 오래된 채무부터 순차 소진할 뿐이라 k개월분이 한 번에 전액 충당된다(§7 실측 참고).
5. GL 전표 계약 (usePurchaseJournal.js)
P1 — 매입확정 (vouchersPurchase(), id = pj-pur-{purchase.id})
| 라인 | 계정 | 금액 |
|---|---|---|
| DR | purchase.accountCode(사용자 선택 비용/자산) | supplyAmount |
| DR | 0135(부가세대급금) — taxType==='과세'이고 vatAmount>0일 때만 | vatAmount |
| CR | purchase.payableAccountCode(0251 재화 / 0253 용역·경비) | payable (= supplyAmount+vatAmount) |
P2 — 지급 (vouchersPayment(), id = pj-pay-{payment.id})
| 라인 | 계정 | 금액 |
|---|---|---|
| DR | payableAccountCode(그룹핑 — 아래) | 계정별 합산 충당액 |
| CR | 0103(보통예금) | payment.applied |
appliedBreakdown의 각{purchaseId, amount}에 대해 원 매입의payableAccountCode를 역참조해 계정별로 합산한다. 단일 거래처 지급이라도 매입건별 category가 섞이면(예: 같은 거래처가 재화·용역 둘 다 거래)0251·0253두 DR 라인으로 갈라질 수 있다.- 라인 순서는
PAYABLE_ACCOUNT_ORDER = ['0251', '0253']고정(결정적 순서,useBillingJournal.CREDIT_COMPONENT_ORDER선례 동형). - 취소(
status==='취소')된 지급은vouchersPayment()에서 제외된다(전표 자체가 생성되지 않음). - 전기 상태는 이 파일이 관리하지 않는다 —
useGeneralLedger.postedIds(Set<voucher.id>)를 그대로 재사용. 콘솔이post(voucher.id)를 호출. partnerOf(vendorCode) = vendorCode(거래처 명칭 마스터 부재 — 폴백. §8 참고).
6. 불변식
- 전표균형: 모든 P1/P2 전표는 DR합 === CR합(실측:
docs/superpowers/sdd/task-7-report.mdGL대사표에서 재확인 — P1 770,000=770,000·P2 330,000=330,000, 데모 시드 기준). - 미지급 잔액 = Σ발생 − Σ충당(거래처별):
payableBalanceByVendor()의 각 거래처 balance =payablesByVendor()의 outstanding 합 = 완납이 아닌 매입건들의payable - paidByPurchase[id]합.useVendorLedger.ledgerFor()의balance(시계열 running 잔액 최종값)와 항상 일치해야 한다(교차검증 —useVendorLedger.spec.js). - GL 대사:
Σ(P1 전표의 0251/0253 CR) − Σ(P2 전표의 0251/0253 DR)(거래처별 또는 전체) = 서브원장(payableBalanceByVendor()) 합계. 실측 수치는 §7과 task-7-report.md 참고. - payableAccountCode 분기:
category === '재화'→0251(외상매입금). 그 외(용역/경비 전부) →0253(미지급금). 이 분기는recordPurchase()시점에 고정 저장되고(purchase.payableAccountCode), 이후 재계산하지 않는다. - vatAmount 조건부:
taxType!=='과세'면 항상 0(면세/영세/불공제는 부가세대급금 라인 자체가 생성되지 않음 — P1 전표에서 0135 DR 라인 생략). - 회귀0: 신규 축(purchases·payments·paidByPurchase)은 기존 E-시리즈 컴포저블·전 스위트에 영향 없음 —
seedDemoPurchases()/seedDemoPayments()는 main.js 부트스트랩에서만 호출되고 모듈 로드 시 자동 소비되지 않는다(회계/GL/FS 테스트가 컴포저블을 직접 import할 때 매입 데모로 오염되지 않도록).
7. Goldnus 수용 케이스 (블루프린트 §7 레퍼런스)
실증 원천은 goldnus-state cost-recon.json.vendorLedger(미켈란 21개월·매입 737라인) — 이번 슬라이스는 실데이터 미클론이라 그 원형을 본뜬 데모 시드로 검증했다. 데모 시드(demoSeed.js buildPurchases/buildPayments, DEMO_CONFIG.householdCount=15 기준 scale=15/300=0.05):
| 아키타입 | vendorCode | 실측 결과 |
|---|---|---|
| ① 위탁관리비 정액 다월(용역) — 4개월분 계산서 | v-svc-mgmt | payable 440,000(계정 0253) 중 165,000 부분지급(2025-12-10) → FIFO가 1번째 채무(110,000) 전액 + 2번째 채무(110,000) 중 55,000 충당 → 잔액 275,000 |
| ② 재화 매입(외상매입금) — 단건 | v-goods-supply | payable 165,000(계정 0251), 지급 없음 → 잔액 165,000 그대로 |
| ③ 몰아지급(청소용역 6개월분 열림 → 1회 일괄지급) | v-svc-cleaning | payable 165,000(6건×27,500, 계정 0253) 전부 → 1회 지급 165,000(2025-12-28) → FIFO가 6건 각각 27,500씩 정확히 완납 → 잔액 0 |
이 3케이스가 블루프린트 §7 "월 정액 용역 다월 일괄 지급" 수용테스트를 대체 실증한다(k×월 정액 일괄지급 = FIFO 자연 처리, 별도 로직 불필요 — spec §3 확인).
8. As-built vs 후속
As-built(이번 슬라이스, main 예정 2026-07-07): P1(매입확정)·P2(지급확정·FIFO충당·초과차단·취소)·거래처 원장(read-only)·P1/P2 GL 전표(미전기 생성)·회계 전기(useGeneralLedger.post 재사용)·회계 게이팅(useModuleSubscription.accountingEnabled)·/purchasing transaction·payment·vendor-ledger 콘솔 실화.
후속(스코프 아웃, 블루프린트 §2·§8):
- P3(선지급) — 채무 초과 지급은 현재 하드 차단.
0131선급금 계정으로 받아 차기 P1 도래 시 자동 상계 제안(E5 선수금의 미러). - P4(카드 매입) — 귀속일(승인일) vs 결제일(카드대금 인출일) 이원화.
- P5(매입 정정/반품) — P1 역분개(매입환출·에누리 계정 선택), reversal·원거래 참조(E7 동형).
- P6(미식별 출금) — 가지급금
0134임시 계상 후 P2/P3/경비로 대체(E8 가수금의 미러). - 3자통과(전기·수도·열 — 세대 부과 vs 공급기관 납부) 총액법/순액법 에디션 결정(블루프린트 §4).
- 세금계산서 허브 → P1 자동 생성(Phase 4 합류 예정, 현재는 수동 등록만).
- 은행 거래 수집 → P2/P6 후보 매칭(가상계좌 번호 우선 — 상대명 매칭 오분류 실측 사례 있음).
- 3원 대사 리포트(계산서↔지급↔은행 실측 상시 폐합 결산 게이트).
- 거래처 명칭 마스터(현재
vendorCode그대로 전표 partner·콘솔 표시에 폴백 — CLAUDE.md 코드체계상 "등록코드/이름" 노출 정책은 거래처 마스터 도입 시 정합 예정). - 계정 기본값 학습(거래처×항목 → 계정 추천, YAGNI로 이번 슬라이스 제외 — 오너 승인).
- 지급 취소(cancelPayment) 화면 버튼 배선(컴포저블은 있음).