주문은 100건인데 정산은 98건이다 · 금액이 갈라지는 자리 · 실습
혜택을 품목마다 나누고 부분 취소를 다시 계산하기
목표
주문 단위로 붙은 할인·쿠폰·포인트·배송비를 품목마다 나누어 붙이고, 1원이 어디로 가는지 직접 정합니다. 그다음 품목 하나를 취소했을 때 그 배분이 왜 다시 계산돼야 하는지 숫자로 확인합니다.
왜 중요한가
혜택은 주문에 붙고 환불은 품목으로 나갑니다. 5,000원 쿠폰을 쓴 주문에서 품목 하나만 취소하면 얼마를 돌려줘야 할까요. 정가를 돌려주면 고객은 쿠폰만큼 이득을 보고 우리는 그만큼 손해를 봅니다. 그래서 주문에 붙은 혜택을 품목마다 나눠 두고, 환불은 그 실판매가 로 합니다. 이 배분표가 없으면 부분 취소·부분 반품·판매자 정산이 전부 근사값이 됩니다.
나누면 반드시 나머지가 남습니다. 5,000원을 세 품목에 금액 비율로 나누면 소수점이 나오고, 그냥 내림하면 몇 원이 사라집니다. 사라진 돈은 어디에도 없으므로 배분표의 합이 실결제 금액과 달라지고, 그 차이가 정산 대사에서 원인을 알 수 없는 잔액으로 나타납니다. 그래서 배분 함수의 첫 번째 성질은 나눈 값의 합이 나누기 전과 같다 는 것입니다.
부분 취소는 배분을 한 번 더 흔듭니다. 쿠폰에 "5만원 이상 구매 시" 같은 조건이 붙어 있으면, 품목을 빼는 순간 남은 주문이 그 조건을 못 채울 수 있습니다. 그러면 이미 준 혜택을 되가져와야 하고, 배송비 무료 조건도 마찬가지로 깨집니다. 환불액이 그 품목의 실판매가보다 작아지는 이유가 이것이고, 심하면 돌려줄 돈이 아예 없어집니다. 이 계산을 안 해 두면 "왜 5만원짜리를 취소했는데 3만원만 들어오나요" 라는 문의에 답할 수 없습니다.
이 실습이 정한 값
배분 방식과 혜택 회수 규칙은 회사의 프로모션 정책에 있습니다. 이 실습에서는 아래로 못 박습니다.
- 주문에 붙은 네 값(
order_discount,coupon_amount,point_used,ship_fee)을 품목의item_amount비율로 나눕니다. - 나누는 방법은 최대잔여법입니다. 각 품목 몫은
총액 × 품목금액 ÷ 상품합계의 내림이고, 남은 1원씩을 나머지가 큰 품목부터 줍니다. 나머지가 같으면item_seq가 작은 품목이 먼저입니다. - 품목 실판매가
net_amount=item_amount+ 배송비 몫 − 할인 몫 − 쿠폰 몫 − 포인트 몫. 한 주문의 실판매가 합은paid_amount와 정확히 같아야 합니다. - 부분 취소 환불액은 그 품목의
net_amount에서 시작합니다. - 쿠폰 회수: 취소 뒤 남은 상품 합계가
coupon_min에 못 미치면, 남은 품목에 붙어 있던 쿠폰 몫(쿠폰 총액에서 취소 품목의 쿠폰 몫을 뺀 값)을 환불액에서 뺍니다. - 배송비 재부과: 원래 배송비가 0원이었는데 남은 상품 합계가 50,000원에 못 미치면 3,000원을 환불액에서 뺍니다. 원래 배송비를 받은 주문은 그대로 둡니다.
- 회수 뒤 환불액이 음수가 되면 0원으로 멈춥니다. 고객에게서 돈을 더 받지는 않습니다.
단계
1. 오른쪽 예시의 생성기를 /root/com/make_alloc.py 로 저장하고 python3 make_alloc.py 로 실행하세요. alloc_orders.csv(주문 13건), alloc_items.csv(품목 36줄), cancel_req.csv(부분 취소 요청 12건)가 만들어집니다.
2. /root/com/alloc.py 에 allocate(total, weights) 함수를 쓰세요. 정수 목록을 돌려주고, 합은 언제나 total 과 같아야 합니다. 채점기가 이 함수를 직접 불러 재료에 없는 입력으로도 시험합니다.
3. 품목마다 배분 결과를 /root/com/item_alloc.csv 에 order_id,item_seq,item_amount,discount_share,coupon_share,point_share,ship_share,net_amount 머리글로 적으세요.
4. /root/com/checksum.txt 에 orders=, items=, net_sum_ok=, rounding_items=, naive_gap_total= 다섯 줄을 적으세요. net_sum_ok 는 실판매가 합이 paid_amount 와 같은 주문의 수, rounding_items 는 네 혜택 중 하나라도 1원을 더 받은 품목 줄의 수, naive_gap_total 은 내림만 하고 잔돈을 나눠 주지 않았을 때 사라지는 금액의 총합입니다.
5. 부분 취소 요청마다 회수 전 환불액을 /root/com/refund_before.csv 에 order_id,item_seq,net_amount 머리글로 적으세요.
6. 요청마다 남은 상품 합계와 회수액을 /root/com/clawback.csv 에 order_id,item_seq,left_goods,coupon_clawback,reship_fee 머리글로 적으세요.
7. 최종 환불액을 /root/com/refund_final.csv 에 order_id,item_seq,refund 머리글로 적고, /root/com/refund_final.txt 에 before_total=, clawback_total=, reship_total=, floored_count=, after_total= 다섯 줄을 적으세요.
8. /root/com/alloc_report.md 에 ## 혜택을 나누는 방법, ## 1원이 가는 곳, ## 부분 취소가 배분을 다시 건드리는 이유, ## 시스템에 남겨야 할 것 네 절로 보고서를 쓰세요.
참고
- 파이썬의
//는 내림 나눗셈입니다.a * b // c처럼 곱을 먼저 하면 정밀도를 잃지 않습니다. 자세한 사정은 [파이썬 문서의 부동소수점 산술](https://docs.python.org/3/tutorial/floatingpoint.html) 에 있습니다. - 흔한 실수 1: 비율을
0.08같은 실수로 들고 다니는 것. 이진 부동소수점은 그 값을 정확히 담지 못해 마지막 1원이 어긋납니다. - 흔한 실수 2: 잔돈을 첫 품목에 몰아 주는 것. 합은 맞지만 같은 값이 들어와도 결과가 달라질 수 있어 재계산할 때마다 배분표가 바뀝니다.
- 흔한 실수 3: 쿠폰 회수액을 쿠폰 총액 전부로 잡는 것. 취소된 품목에 붙어 있던 몫은 이미 환불액에서 빠져 있으므로 두 번 빼게 됩니다.
단계 8개
- 주문·품목·취소 요청 데이터 만들기
- 합이 보존되는 배분 함수 쓰기
- 품목마다 혜택 나눠 붙이기
- 사라질 뻔한 잔돈 재기
- 부분 취소 환불액 구하기 (회수 전)
- 깨진 혜택 조건 되가져오기
- 최종 환불액과 합계 내기
- 금액 배분 점검 보고서 쓰기