지난주 결제액에서 환불액을 뺐는데 AI 보고서 두 개가 서로 다른 금액을 낸다. 한쪽은 지난주에 처리된 환불을 모두 빼고, 다른 쪽은 지난주 주문에 연결된 환불만 뺐다면 계산 오류가 아닐 수 있다. 어느 날 발생한 돈의 변화를 볼지, 어느 기간의 주문을 추적할지 먼저 정해야 한다.
이 글은 AI로 매출 집계 보고서를 만들거나 검토하는 독자를 위한 실습이다. 덧셈·뺄셈과 날짜 구간만 알면 본문을 읽을 수 있고 Python 코드는 선택 사항이다. 주문·환불·금액은 모두 가상 자료다. 같은 기록에서 9만 원과 14만 원이 각각 나오는 이유를 계산하고, AI에 어떤 날짜 기준을 명시해야 하는지 정리한다.
먼저 두 가지 질문을 분리한다
첫 질문은 “11월 1일부터 7일까지 결제된 금액에서 같은 기간에 환불된 금액을 빼면 얼마인가?”다. 여기서는 결제와 환불을 각각 발생한 날짜로 고른다. 10월 주문의 환불이라도 11월 2일에 처리됐다면 포함한다.
두 번째 질문은 “11월 1일부터 7일까지 결제된 주문들에서, 11월 8일 0시 전까지 환불된 금액을 빼면 얼마가 남는가?”다. 먼저 주문 묶음을 고르고 그 주문에 속한 환불만 연결한다. 같은 기간에 결제됐다는 공통 조건으로 묶은 주문 집단을 코호트라고도 부른다. 이 글에서는 주문 묶음이라고 부르겠다.
아래 금액은 한 통화의 상품 결제·환불 기록을 비교하기 위한 운영 지표다. 회계상 수익 인식, 세금 신고 매출, 결제대행사의 실제 입금액을 정의하는 규칙은 아니다. 수수료·배송비·세금·차지백·환불 실패·통화 환산은 제외하고, 환불은 완료된 양수 금액으로 기록한다고 가정한다.
10월 주문의 환불 5만 원이 차이를 만든다
모든 날짜는 한국 시간의 달력 날짜다. 비교 구간은 2026년 11월 1일 이상, 11월 8일 미만이다. 이처럼 시작은 포함하고 끝은 제외하면 연속된 기간을 붙여도 경계일을 두 번 세지 않는다.
| 주문 | 결제일·금액 | 환불일·금액 |
|---|---|---|
| A | 11/1 · 100,000원 | 11/3 · 20,000원 |
| B | 10/30 · 50,000원 | 11/2 · 50,000원 |
| C | 11/5 · 60,000원 | 11/8 · 30,000원 |
기간 안에 결제된 주문은 A와 C다. 결제액 합계는 16만 원이다. 같은 기간에 발생한 환불은 A의 2만 원과 B의 5만 원이므로 7만 원이다. 발생일 기준 차액은 16만 − 7만 = 9만 원이다. C의 환불은 11월 8일이므로 이 구간에 들어가지 않는다.
주문 묶음 기준에서는 A와 C에 연결된 환불만 본다. 11월 8일 0시 전까지 해당하는 것은 A의 2만 원뿐이다. 따라서 잔액은 16만 − 2만 = 14만 원이다. B의 환불 5만 원은 이번 기간에 처리됐지만, 10월 주문에 속하므로 이 질문에서는 제외한다.

주문 묶음의 잔액에는 관측 마감이 붙어야 한다
같은 A·C 주문 묶음을 11월 9일 0시 기준으로 다시 보면 C의 환불 3만 원도 포함된다. 잔액은 14만 원에서 11만 원으로 바뀐다. 주문 기간이 달라진 것이 아니라, 그 주문들의 환불을 더 오래 지켜본 것이다. 반면 11월 1~7일 발생일 기준 차액은 이 예제에서 여전히 9만 원이다.
따라서 “지난주 주문의 순액은 14만 원”만 적으면 나중에 재현하기 어렵다. “11월 1~7일 결제 주문, 11월 8일 0시 전까지 완료된 환불 반영: 14만 원”처럼 주문 기간과 환불 마감을 함께 적는다. 실제 수집 시스템에서는 발생일과 자료가 들어온 시각도 다를 수 있다. 아래 코드는 이미 제공된 목록을 날짜로 고를 뿐, 지연 수집이나 원본 수정 이력을 재구성하지 않는다.
AI에게 줄 요청문도 질문에 맞춰 바꾼다
운영 보고서 요청 예시 대상: 원화 상품 결제와 완료 환불. 세금·배송비·수수료 제외. 시간대: Asia/Seoul. 날짜는 시작 포함, 종료 제외. 1. 11/1 이상 11/8 미만 결제액과 같은 기간 환불액의 차액을 계산해 주세요. 2. 같은 기간 결제된 주문만 고르고, 해당 주문의 11/8 전 완료 환불을 빼 주세요. 두 결과의 이름을 구분하고, 차이를 만든 주문 ID와 금액을 보여 주세요. 이 숫자를 회계상 매출이나 실제 입금액으로 바꿔 부르지 마세요.
AI가 “환불 때문에 이번 주 주문 품질이 나빠졌다”고 요약한다면, 먼저 환불의 원주문 기간을 확인한다. 이번 주 환불 증가가 지난달 주문에서 나왔다면 이번 주에 받은 주문의 품질을 곧바로 설명하지 못한다. 반대로 최근 주문 묶음은 환불이 발생할 시간이 짧았으므로 관측된 환불이 적어 주문 품질이 더 좋아 보일 수 있다. 주문 품질을 비교하려면 결제 후 같은 길이의 관측 기간 등 별도의 비교 설계가 필요하다.
두 기준을 계산하는 전체 코드
아래 코드를 refund_basis.py라는 UTF-8 파일로 저장해 python3 refund_basis.py로 실행한다. Python 표준 라이브러리만 사용한다. date.fromisoformat으로 날짜를 읽고, 다시 표준 문자열과 비교해 YYYY-MM-DD 형식으로 제한한다. 주문 ID와 환불 ID는 각각 유일해야 하며, 한 주문에 여러 부분 환불이 있는 것은 허용한다.
from datetime import date
ORDERS = [('A', '2026-11-01', 100000),
('B', '2026-10-30', 50000),
('C', '2026-11-05', 60000)]
REFUNDS = [('R1', 'A', '2026-11-03', 20000),
('R2', 'B', '2026-11-02', 50000),
('R3', 'C', '2026-11-08', 30000)]
def day(text):
value = date.fromisoformat(text)
if value.isoformat() != text:
raise ValueError('날짜는 YYYY-MM-DD 형식이어야 합니다')
return value
def compare(orders, refunds, start, end, cutoff):
start, end, cutoff = map(day, (start, end, cutoff))
if not start < end <= cutoff:
raise ValueError('시작 < 종료 <= 관측 마감이어야 합니다')
ledger = {}
for oid, when, amount in orders:
if oid in ledger or type(amount) is not int or amount <= 0:
raise ValueError('중복 주문 또는 잘못된 금액')
ledger[oid] = (day(when), amount)
seen, returned = set(), {oid: 0 for oid in ledger}
event_refund = cohort_refund = 0
cohort = {oid for oid, (when, _) in ledger.items()
if start <= when < end}
for rid, oid, when, amount in refunds:
when = day(when)
if rid in seen or oid not in ledger:
raise ValueError('중복 환불 또는 원주문 없음')
seen.add(rid)
if type(amount) is not int or amount <= 0 or when < ledger[oid][0]:
raise ValueError('잘못된 환불 금액 또는 날짜')
returned[oid] += amount
if returned[oid] > ledger[oid][1]:
raise ValueError('누적 환불이 주문 금액을 초과합니다')
if start <= when < end:
event_refund += amount
if oid in cohort and when < cutoff:
cohort_refund += amount
gross = sum(ledger[oid][1] for oid in cohort)
return {'기간_결제액': gross,
'기간_환불액': event_refund,
'발생일_차액': gross - event_refund,
'선택주문_환불액': cohort_refund,
'주문묶음_잔액': gross - cohort_refund}
if __name__ == '__main__':
print(compare(ORDERS, REFUNDS, '2026-11-01', '2026-11-08', '2026-11-08'))
기본 출력은 기간 결제액 160000, 기간 환불액 70000, 발생일 차액 90000, 선택 주문 환불액 20000, 주문 묶음 잔액 140000이다. 마지막 마감 날짜만 2026-11-09로 바꾸면 주문 묶음 잔액이 110000이 된다. 이 코드는 입력된 전체 목록의 기본 정합성부터 검사하므로 마감 밖의 환불도 원주문 없음·중복·초과 환불이면 오류를 낸다.
자료를 바꿔 세 가지를 확인해 보자
첫째, B의 환불을 목록에서 빼면 두 기준 모두 14만 원이 된다. 차이를 만들던 과거 주문의 환불이 없어졌기 때문이다. 둘째, A의 2만 원 환불을 서로 다른 ID의 1만 원 환불 두 개로 나누면 결과는 같아야 한다. 셋째, 같은 환불 ID를 복사하면 오류가 나야 한다. 부분 환불 두 건과 같은 환불의 중복 수집은 다른 상황이다.
원주문을 찾을 수 없는 환불을 그냥 버려서 합계를 맞추지는 않는다. 발생일 기준 보고서에 필요한 과거 주문이 조회 범위 밖에 있는지 확인해야 한다. 예제는 모든 환불의 원주문을 제공하는 작은 목록을 전제로 한다. 실제 데이터에서는 필요한 과거 주문까지 확보하거나, 원주문 검증과 기간별 금액 집계를 나누는 설계가 필요하다.
확인한 범위와 다음에 읽을 글
2026년 11월 9일 Linux·Python 3.12.14에서 합성 자료로 본문 코드를 실행했다. 기본 두 합계, 마감 변경, 과거 주문 환불 제거, 부분 환불 분할, 중복 환불·원주문 누락·초과 환불·역전된 기간의 오류 처리를 확인했다. 실제 결제 서비스·회계 시스템·AI 모델 호출은 시험하지 않았다.
같은 날 Python 3.12 공식 datetime 문서에서 날짜 파싱과 date 비교 동작을 확인했다. 두 집계 기준과 요청문은 이 글의 학습용 설계이며 공식 회계 기준을 인용한 것이 아니다.
환불이 없는 집계에서 주문 수와 주문당 금액의 변화를 계산하는 방법은 매출 변화 분해 실습, 어떤 자료를 다음에 읽을지 고르는 과정은 매출 조사 행동 선택 실습에서 이어 볼 수 있다.
두 보고서가 9만 원과 14만 원으로 다르면 AI에게 곧바로 다시 계산하라고 하지 말자. 먼저 10월 주문 B의 환불 5만 원을 어느 쪽에 넣었는지 찾으면 된다. 보고서의 질문과 날짜 기준을 맞춘 뒤에야 숫자가 서로 비교 가능한 답이 된다.