Skip to content

매입 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).

단일 소스 원칙: paidByPurchaseusePurchases 파일 내부에만 있고, usePayments/useVendorLedger/usePurchaseJournal 어디에서도 직접 쓰지 않는다 — 전부 pay()/unpay()/payablesByVendor()/payableBalanceByVendor() 파생 함수를 통해서만 읽고 쓴다.

4. FIFO 충당 + 초과 차단 (P2 핵심)

usePayments.recordPayment():

  1. amount>0 가드(콘솔 폼 검증 우회 대비): !(Number(amount) > 0){ ok:false, reason:'지급 금액은 0보다 커야 합니다', applied:0 }, 아무것도 기록 안 함.
  2. 초과 차단 가드(spec §3): amount > payableBalanceByVendor()의 해당 거래처 balance{ ok:false, reason:'채무 초과 — 선지급(P3)은 후속 슬라이스', applied:0 }. pay()를 아예 호출하지 않는다 — 부분충당 후 거부(원자성 위반) 금지. usePurchases.pay() 자체는 정책 판단 없는 무음 클램프이므로, 가드는 반드시 호출 "전"에 이 파일(usePayments)에서 검사한다.
  3. 가드 통과 시: peekPaid(vendorCode)로 before 스냅샷 → pay(vendorCode, amount)(FIFO, 오래된 purchaseDate 우선) → peekPaid(vendorCode)로 after 스냅샷 → diff(after - before, 0보다 큰 것만)로 appliedBreakdown 구성.
  4. 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})

라인계정금액
DRpurchase.accountCode(사용자 선택 비용/자산)supplyAmount
DR0135(부가세대급금) — taxType==='과세'이고 vatAmount>0일 때만vatAmount
CRpurchase.payableAccountCode(0251 재화 / 0253 용역·경비)payable (= supplyAmount+vatAmount)

P2 — 지급 (vouchersPayment(), id = pj-pay-{payment.id})

라인계정금액
DRpayableAccountCode(그룹핑 — 아래)계정별 합산 충당액
CR0103(보통예금)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. 불변식

  1. 전표균형: 모든 P1/P2 전표는 DR합 === CR합(실측: docs/superpowers/sdd/task-7-report.md GL대사표에서 재확인 — P1 770,000=770,000·P2 330,000=330,000, 데모 시드 기준).
  2. 미지급 잔액 = Σ발생 − Σ충당(거래처별): payableBalanceByVendor()의 각 거래처 balance = payablesByVendor()의 outstanding 합 = 완납이 아닌 매입건들의 payable - paidByPurchase[id] 합. useVendorLedger.ledgerFor()balance(시계열 running 잔액 최종값)와 항상 일치해야 한다(교차검증 — useVendorLedger.spec.js).
  3. GL 대사: Σ(P1 전표의 0251/0253 CR) − Σ(P2 전표의 0251/0253 DR)(거래처별 또는 전체) = 서브원장(payableBalanceByVendor()) 합계. 실측 수치는 §7과 task-7-report.md 참고.
  4. payableAccountCode 분기: category === '재화'0251(외상매입금). 그 외(용역/경비 전부) → 0253(미지급금). 이 분기는 recordPurchase() 시점에 고정 저장되고(purchase.payableAccountCode), 이후 재계산하지 않는다.
  5. vatAmount 조건부: taxType!=='과세'면 항상 0(면세/영세/불공제는 부가세대급금 라인 자체가 생성되지 않음 — P1 전표에서 0135 DR 라인 생략).
  6. 회귀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-mgmtpayable 440,000(계정 0253) 중 165,000 부분지급(2025-12-10) → FIFO가 1번째 채무(110,000) 전액 + 2번째 채무(110,000) 중 55,000 충당 → 잔액 275,000
② 재화 매입(외상매입금) — 단건v-goods-supplypayable 165,000(계정 0251), 지급 없음 → 잔액 165,000 그대로
몰아지급(청소용역 6개월분 열림 → 1회 일괄지급)v-svc-cleaningpayable 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) 화면 버튼 배선(컴포저블은 있음).