B2B SaaS invoice adjustment evidence schema 2026: credit note·refund의 원 invoice·line item 연결 필드와 예외 코드

B2B SaaS invoice adjustment evidence schema 2026: credit note·refund의 원 invoice·line item 연결 필드와 예외 코드


B2B SaaS billing에서 refundcredit note가 생성됐다는 사실만으로는 운영 증거가 완성되지 않습니다. 조정된 금액이 어느 원 invoice에 속하는지, 여러 line item 중 무엇을 줄였는지, 원 결제 객체와 연결되는지, 그 연결이 공급사의 원문 필드인지 내부 추론인지가 함께 남아 있어야 합니다.

이 글은 일반적인 환불 추적이나 환불 승인 절차를 설명하지 않습니다. 최종 invoice 이후 발생한 credit note·refund 조정 사건을 원 invoice·line item·원 transaction에 연결하고, 그 근거와 불확실성을 같은 레코드에 저장하는 evidence schema가 주제입니다. 예시는 모두 가상 데이터입니다.

회계·세무·법률 조언, 매출 인식 결론, 특정 환불 승인 정책을 제공하지 않습니다. 이 문서는 API 응답과 내부 운영 로그를 다시 확인할 수 있게 만드는 데이터 품질·증거 보존용 체크리스트입니다. 공급사 API 버전, 계정 설정, 국가별 기능, export 형태에 따라 실제 필드가 달라질 수 있으므로 원문 응답과 현재 공식 문서를 기준으로 검증하세요.

기존 글과 이 글의 경계

비슷한 키워드가 있어도 확인하는 레이어가 다릅니다. 아래 글을 먼저 읽어야 하는 경우와 이 글에서 이어서 확인할 경우를 분리해 두면 같은 refund를 여러 보고서에서 다른 사건으로 복제하는 일을 줄일 수 있습니다.

  • B2B SaaS 환불·크레딧 노트 추적 감사 체크리스트는 refund·credit note·proration을 churn·운영 보고서와 분리하는 업무 분류와 추적 관점이 중심입니다. 이 글은 그 사건을 어떤 ID와 증거 필드로 재현할지에만 집중합니다. (공개글 30264)
  • B2B SaaS 구독 청구 월말 대사 체크리스트는 invoice·payment·payout·ERP·은행의 월말 대사와 cutoff가 중심입니다. 이 글은 payout이나 은행 입금으로 확장하지 않고, invoice adjustment가 원 문서에 연결되는지까지만 봅니다. (공개글 30827)
  • 결제 webhook replay 검증 로그·대사 리포트 템플릿은 event ledger와 replay, provider object 상태를 재처리·대사하는 템플릿입니다. 이 글의 evidence_snapshot_ref는 webhook 재처리 설계가 아니라 adjustment 매핑 결과를 뒷받침하는 원문 보관 키입니다. (공개글 31227)
  • Chargebee Unbilled Charges API 대사 체크리스트는 최종 invoice 전 unbilled_charge → invoice line item 생명주기를 다룹니다. 이 글은 최종 invoice가 이미 존재한 뒤 credit note·refund가 어느 line item을 조정했는지 확인합니다. (공개글 31305)

따라서 unbilled charge 생성, usage·meter, proration 계산, cancellation·churn, CRM·GA4 전송, webhook replay, dunning, payout·ERP·은행 월말 대사는 이 글의 판단 대상이 아닙니다. 그런 상태를 원인이나 후속 시스템의 입력으로 보관할 수는 있지만, 여기서 새 정책을 정의하지는 않습니다.


먼저 adjustment evidence 레코드의 단위를 고정합니다

한 invoice에 여러 번의 부분 환불이나 여러 credit note가 생길 수 있고, 한 credit note가 여러 line item을 조정할 수 있습니다. 따라서 invoice_id 한 열을 업데이트하는 방식 대신 adjustment 1건과 그 adjustment가 가리키는 원본 1건 이상의 연결 edge를 저장하는 편이 안전합니다.


[adjustment object]
        │
        ├── [original invoice]
        │          └── [original invoice line item, optional]
        │
        └── [original payment / charge / transaction, optional]
                    │
             [evidence snapshot + mapping result]

여기서 optional은 중요합니다. provider 원문에 원 line item ID가 없는데도 금액·날짜·설명으로 임의 생성하면 직접 연결처럼 보입니다. 연결이 없다는 사실을 invoice_link_missing 또는 line_item_link_missing으로 남기고, 추론으로 얻은 연결은 mapping_method=indirect로 구분합니다.

공통 정규화 schema

provider별 원문 필드명은 달라도 내부 저장소에는 같은 의미의 필드를 둡니다. 원문 JSON은 별도 보관하고, 정규화 레코드는 검색·검증에 사용합니다.

공통 필드 용도 기록 규칙
schema_version 내부 스키마 버전 필드 변경 때 증가. provider API 버전과 섞지 않음
provider stripe, chargebee, paddle, recurly 소문자 enum으로 제한
environment test 또는 live 테스트 객체와 실데이터 혼입 방지
adjustment_type credit_note, refund, credit_invoice, adjustment provider의 원문 object type을 별도 보존
provider_origin provider가 구분하는 조정·credit 발생 원인 refund, termination, immediate_change, credit, write_off 등을 원문 값으로 보존. original_invoice_id와 동일시하지 않음
adjustment_id 조정 객체의 공급사 ID 사람이 읽는 번호 대신 원본 ID를 기본 키로 사용
adjustment_status 조정 객체의 현재 상태 요청·완료를 임의로 합치지 않음
original_invoice_id 원 invoice의 provider ID 직접·간접·legacy 결과를 mapping_method로 설명
original_invoice_number 사람이 확인하는 invoice 번호 ID를 대신하는 유일 키로 사용하지 않음
original_line_item_id 조정된 원 line item ID 없으면 빈 값과 예외 코드를 함께 저장
original_transaction_id 원 transaction·charge·payment ID object type도 함께 저장
original_payment_id 결제 시도 또는 payment 객체 ID refund의 결제수단 연결과 invoice 연결을 혼동하지 않음
amount_minor 조정 금액의 최소 화폐 단위 정수 소수 금액과 minor unit 규칙을 provider별로 확인
currency 조정 금액의 ISO 통화 코드 대문자 표준화. 환율 계산은 이 글의 범위 밖
adjustment_line_amount_minor 특정 line에 배분된 금액 총액과 line 합계 검증용
source_created_at provider 원문 생성 시각 원문 timezone 또는 UTC 변환 규칙 보존
retrieved_at 원문을 조회한 시각 현재 상태를 언제 확인했는지 재현
mapping_method direct, indirect, legacy, unknown 연결 경로를 한 값으로 명시
mapping_confidence high, medium, low, none 근거가 약할수록 낮춤. 임의 보정 금지
exception_codes 0개 이상의 표준 예외 코드 미해결 예외를 빈 배열로 덮지 않음
evidence_locator API endpoint, dashboard URL 또는 저장소 key 비밀 토큰·고객 개인정보를 URL에 넣지 않음
evidence_snapshot_ref 원문 JSON/PDF/응답 해시의 보관 키 원문 접근권한과 보존 기간은 별도 관리
provider_api_version 조회에 사용한 API·export 버전 legacy 판정을 재현하는 단서
raw_object_hash 저장한 원문 무결성 확인값 원문 자체를 바꾸지 않고 변경 감지에 사용

adjustment_line_amount_minor를 합산해 amount_minor와 비교할 때는 provider의 discount·tax·rounding 항목을 별도로 보존합니다. 차이가 난다고 즉시 오류로 고정하지 말고, 원문이 line별 금액을 제공하는지부터 확인하세요. 다만 설명만으로 차이를 정상 처리하지 말고, 기준을 정하지 못하면 amount_mismatch를 남깁니다.

mapping method와 confidence를 분리하는 법

direct는 provider 원문에 adjustment와 원 invoice 또는 원 line item을 가리키는 필드가 있고, 그 ID를 그대로 대조한 경우입니다. indirect는 adjustment가 가리키는 transaction·charge·payment를 따라가서 invoice를 찾은 경우입니다. legacy는 현재 권장 경로가 아닌 구 API 응답, 과거 export, 이전 object 관계를 사용한 경우입니다. unknown은 후보 매칭조차 확정하지 못한 상태입니다.

방법 high medium low
direct 원 invoice ID와 원 line item ID가 모두 원문에 있고 현재 객체와 일치 invoice ID는 직접이나 line item은 일부만 존재 원문 필드가 선택적이거나 API 버전 확인이 안 됨
indirect transaction→invoice와 line item ID가 각각 재조회로 일치 transaction→invoice는 일치하나 line item은 설명·금액으로만 후보 금액·날짜·고객 조합만 일치
legacy legacy 필드와 원문 snapshot, API 버전이 모두 보존됨 legacy 필드는 있으나 현재 객체 재조회가 제한됨 과거 export의 번호·설명만 남음
unknown 해당 없음 해당 없음 연결 후보가 없거나 충돌함

직접 필드가 없을 때 mapping_confidence=high로 올릴 수 있는 조건을 팀에서 먼저 문서화하세요. 예를 들어 고객 ID, 통화, 금액, invoice 기간이 모두 맞아도 동일 금액의 line이 두 개면 line item 연결은 low이거나 line_item_link_missing이어야 합니다.

Provider별 연결 경로: 직접·간접·legacy

다음 표는 내부 schema에 넣을 mapping rule의 출발점입니다. “직접”은 해당 provider의 해당 object 응답에 필드가 실제로 존재하고 값이 채워졌을 때만 적용합니다. 빈 필드를 내부 계산으로 채웠다면 직접으로 바꾸지 않습니다.

Provider 직접 연결 간접 연결 legacy·주의 경로
Stripe Credit Note의 invoice; credit note line의 invoice_line_item이 제공되는 경우 original_invoice_id·original_line_item_id에 그대로 매핑 Refund의 charge 또는 payment_intent에서 원 결제 객체를 찾고, 그 객체와 invoice 관계를 재조회. line item은 별도 원문 관계가 없으면 미연결로 유지 구 API·object 버전에서 charge.invoice가 채워지는 경로는 API 버전과 snapshot을 저장한 뒤 legacy로 표시. 금액·설명만으로 line을 확정하지 않음
Chargebee Credit Note의 reference_invoice_id와 line의 원 line-item 참조가 응답에 있을 때 직접 매핑. 계정/API 버전에서 refund가 invoice 참조를 제공하면 그 값도 직접 기록 Refund 또는 transaction의 payment_id·transaction 관계를 따라 invoice를 재조회. invoice line은 credit note·invoice 원문에서 ID를 다시 대조 구 API·CSV export에서 invoice 번호나 오래된 참조 필드만 남은 경우 legacy. 현재 API로 재조회 가능한지 확인하고 번호만으로 ID를 만들지 않음
Paddle adjustment.transaction_id를 1차 연결 키로 사용하고, partial adjustment는 items[].item_id를 원 transaction item ID로 저장. invoice_id가 실제 응답에 있더라도 transaction_id 경로와 별도 보관 transaction_id → transaction 상세의 invoice 관계 → transaction item으로 이동. item ID가 없으면 line은 간접 후보로만 남김. 두 연결 값이 충돌하면 transaction_id 기준으로 재조회 구형 응답·export의 invoice_id 또는 invoice number는 deprecated/legacy로 별도 보관하며 주 키를 대체하지 않음. 충돌·재조회 실패는 mapping_unknown 또는 evidence_incomplete로 기록. Billing API와 Paddle Classic 모델을 합치지 않음
Recurly Credit invoice의 원 invoice·원 line-item 참조가 응답에 있는 경우 직접 매핑. originoriginal_invoice_id와 별도로 provider_origin에 저장하고 credit invoice를 refund와 동일 object로 저장하지 않음 Refund의 transaction·payment·invoice 번호 관계를 따라 원 invoice를 찾고, line item은 provider가 반환한 원 참조가 있을 때만 연결 origin/export의 invoice_typerefund, termination, immediate_change, credit, write_off 등으로 구분. credit은 standalone credit일 수 있으므로 모든 credit invoice를 원 invoice 환불로 취급하지 않음. 구 XML·CSV 또는 invoice number 중심 export는 legacy로 별도 저장

Stripe의 refund처럼 provider가 결제 객체 연결은 주지만 invoice line 연결은 주지 않는 경우가 있습니다. 그 레코드는 “refund가 어느 결제를 되돌렸는가”와 “어느 invoice line을 조정했는가”를 두 개의 판정으로 나눠야 합니다. 전자를 확인했다고 후자를 자동으로 high로 올리면 안 됩니다.

공식 문서에서 필드 의미를 확인합니다

아래는 작성 시점에 확인한 각 provider의 공식 문서입니다. 링크된 문서의 현재 object schema와 사용 중인 API 버전을 함께 대조하세요.

공식 문서가 어떤 객체의 관계를 설명한다는 것과 우리 계정의 모든 응답에 값이 들어온다는 것은 다릅니다. null, 권한에 따른 부분 응답, API 버전 차이를 evidence에 남기고, 누락을 성공 매핑으로 보정하지 않습니다.


예외 코드는 연결 실패의 종류를 보존합니다

예외 코드는 단순한 “실패” 라벨이 아닙니다. 어떤 edge가 끊겼고 무엇을 다시 조회해야 하는지 알려주는 재처리 입력입니다. 한 레코드에 여러 코드를 넣을 수 있습니다.

코드 판정 기준 운영 확인
mapping_unknown provider·object·API 버전은 알지만 연결 경로를 확정하지 못함 원문 재조회, schema version, object type 확인
invoice_link_missing adjustment와 원 invoice를 잇는 직접·간접 키가 없음 transaction·payment 재조회. 임의 invoice ID 생성 금지
line_item_link_missing invoice는 찾았지만 조정된 원 line item ID가 없음 line-level 원문·credit note line·export 확인
amount_mismatch 조정 총액과 line 합계 또는 연결된 원문 금액이 기준을 벗어남 minor unit, tax·discount·rounding, partial adjustment 대조
currency_mismatch adjustment와 원 invoice·transaction의 통화가 다르거나 값이 없음 통화별로 분리 비교. 환율로 자동 상계하지 않음
provider_object_missing 저장된 ID의 provider object를 재조회할 수 없음 삭제·권한·환경(test/live)·API endpoint 확인
evidence_incomplete snapshot, 조회 시각, API 버전, 원문 해시 중 필수 증거가 부족함 원문 저장·접근권한·보존 키를 보완

예를 들어 invoice_link_missingevidence_incomplete은 함께 생길 수 있습니다. invoice ID를 찾지 못한 원인이 실제 관계 부재인지, 원문 snapshot이 없어 확인할 수 없는 것인지 구분해야 합니다. provider_object_missing은 곧 “원래 존재하지 않았다”는 뜻이 아니므로 삭제나 무효화를 추정하지 않습니다.

가상 데이터 예시

예시 1: Stripe credit note의 직접 line 연결


{
  "schema_version": "2026-01",
  "provider": "stripe",
  "environment": "test",
  "adjustment_type": "credit_note",
  "adjustment_id": "cn_test_1001",
  "original_invoice_id": "in_test_9001",
  "original_line_item_id": "il_test_01",
  "original_transaction_id": null,
  "amount_minor": 12000,
  "currency": "USD",
  "mapping_method": "direct",
  "mapping_confidence": "high",
  "exception_codes": [],
  "evidence_snapshot_ref": "vault://billing-evidence/2026/08/cn_test_1001.json"
}

credit note 원문에 invoice와 invoice line item 참조가 있고, snapshot과 API 버전까지 남아 있으므로 직접·high로 기록한 예입니다. vault:// 값은 가상의 내부 보관 키이며 실제 고객 문서 URL이 아닙니다.

예시 2: Stripe refund는 payment는 연결됐지만 line은 미확정

필드
adjustment_id re_test_2001
adjustment_type refund
original_payment_id pi_test_7001
original_invoice_id in_test_9002
original_line_item_id null
mapping_method indirect
mapping_confidence medium
exception_codes line_item_link_missing

refund가 payment intent와 invoice까지 재조회로 이어졌지만, refund 원문이 어느 line을 조정했는지 말하지 않는다면 line을 금액만으로 배분하지 않습니다. 이 레코드의 목적은 invoice 연결은 확인됐고 line 연결은 아직 없다는 사실을 보존하는 것입니다.

예시 3: Chargebee credit note의 원 invoice·line 참조


{
  "provider": "chargebee",
  "environment": "live",
  "adjustment_type": "credit_note",
  "adjustment_id": "cn_3001",
  "original_invoice_id": "inv_4001",
  "original_line_item_id": "li_4001_02",
  "amount_minor": 5000,
  "currency": "EUR",
  "mapping_method": "direct",
  "mapping_confidence": "high",
  "exception_codes": [],
  "provider_api_version": "v2-export-2026-08",
  "evidence_locator": "api://chargebee/credit_notes/cn_3001"
}

실제 응답에서 해당 원 참조 필드가 채워졌을 때만 이처럼 저장합니다. 필드가 사이트 설정이나 응답 형태에 따라 없으면 invoice_link_missing 또는 line_item_link_missing으로 낮춰 기록합니다.

예시 4: Paddle adjustment의 transaction 경유 매핑

필드
adjustment_id adj_5001
original_transaction_id txn_6001
original_invoice_id null
original_line_item_id null
mapping_method indirect
mapping_confidence low
exception_codes invoice_link_missing, line_item_link_missing

adjustment의 transaction ID는 있으나 transaction 상세에서 invoice ID나 원 item ID를 재현하지 못한 상황입니다. txn_6001을 invoice ID처럼 복사하지 않고, 두 연결 모두 미완료로 남깁니다.

예시 5: Recurly legacy export


provider=recurly
adjustment_type=credit_invoice
adjustment_id=credit-invoice-2026-08-12
original_invoice_number=INV-2026-0711
original_invoice_id=null
original_line_item_id=null
mapping_method=legacy
mapping_confidence=low
exception_codes=evidence_incomplete
provider_api_version=csv-export-legacy

사람이 읽는 invoice number만 있는 과거 export는 현재 API의 invoice ID나 line item ID로 변환했다고 가정하지 않습니다. 원본 파일의 보관 키와 export 시각이 없으면 evidence_incomplete를 추가합니다.

구현 순서: 매핑보다 증거를 먼저 저장합니다

  1. provider, test/live, object type, API 버전을 먼저 기록합니다.
  2. adjustment 원문을 저장하고 adjustment_id, 상태, 생성 시각, 금액, 통화를 추출합니다.
  3. 원문에 있는 invoice·line item·transaction·payment 참조를 직접 필드로 복사합니다.
  4. 직접 참조가 없을 때만 허용한 간접 경로를 따라 원 객체를 재조회합니다.
  5. 각 edge마다 mapping_method, mapping_confidence, 조회 시각, evidence locator를 기록합니다.
  6. invoice 연결과 line item 연결을 별도 판정합니다. 하나가 성공했다고 다른 하나를 성공 처리하지 않습니다.
  7. 총액·line 합계·통화·상태를 비교하고 예외 코드를 누적합니다.
  8. API 재조회 실패는 provider_object_missing, 원문·해시·버전 부족은 evidence_incomplete로 분리합니다.
  9. unresolved queue에는 원문 ID, 마지막 조회 시각, 다음 확인할 경로, 담당 시스템만 남깁니다.
  10. 원문을 보완한 뒤에만 confidence를 올리고, 기존 snapshot을 덮어쓰지 말고 새 조회 버전을 추가합니다.

운영 체크리스트

수집 전

  • [ ] provider와 test/live 환경을 필수 필드로 받는가
  • [ ] adjustment object type별 필수·선택 필드를 schema 문서에 고정했는가
  • [ ] invoice ID와 invoice number를 별도 필드로 저장하는가
  • [ ] 원문 응답, 조회 시각, API·export 버전, 원문 해시를 보관하는가
  • [ ] evidence locator에 API key, 카드번호, 고객 자유 입력, 계약 원문을 넣지 않는가

매핑 중

  • [ ] 직접 필드와 간접 재조회 경로를 provider별로 구분했는가
  • [ ] Stripe refund처럼 payment 연결과 line item 연결이 분리되는 경우를 처리하는가
  • [ ] Chargebee·Paddle·Recurly의 invoice number를 provider object ID로 오인하지 않는가
  • [ ] legacy export가 현재 API의 성공 매핑으로 승격되지 않는가
  • [ ] 고객·통화·금액·기간이 같다는 이유만으로 line item을 확정하지 않는가

검증·보관 후

  • [ ] mapping_unknown, invoice_link_missing, line_item_link_missing을 별도 queue로 집계하는가
  • [ ] amount_mismatch, currency_mismatch를 환율·반올림으로 자동 삭제하지 않는가
  • [ ] provider_object_missing을 원 객체 부재로 단정하지 않는가
  • [ ] evidence_incomplete 레코드가 정상 완료 건수에 섞이지 않는가
  • [ ] snapshot을 덮어쓰지 않고 조회 시각과 변경 이력을 남기는가
  • [ ] 운영 화면에 원문 접근권한을 넓히지 않고, 필요한 ID·상태·예외만 노출하는가

마무리

이 schema의 성공 기준은 모든 refund에 invoice line item을 억지로 붙이는 것이 아닙니다. 직접 연결인지, 다른 객체를 경유한 간접 연결인지, 과거 형식에 의존한 legacy 연결인지, 아직 모르는지를 나중에도 설명할 수 있는 상태가 기준입니다.

처음에는 네 provider 전체를 한 번에 자동화하지 말고, 실제로 가장 많이 발생하는 credit_note 또는 refund 한 종류를 골라 공통 필드와 예외 queue를 검증하세요. 공식 문서의 object 관계, 실제 응답의 nullable 필드, 내부 snapshot을 함께 비교한 뒤 provider별 mapping rule을 버전 관리하면 됩니다. 이 과정은 환불을 승인하는 정책이나 금액의 회계·세무 결론을 대신하지 않으며, 오직 조정 증거의 연결 품질을 높이는 운영 기반입니다.