acquire-core-x/db/schema.d/96-econ-seed.sql
hyeongwoo 438ae8992d Wave G: 실제 매입사 경제모델 고도화 (딥리서치 갭분석 기반)
딥리서치(95 에이전트, OCC/Fed/Berlin Group 등 1차 출처, 검증된 9 finding)로
실제 카드 매입사 아키텍처를 조사(docs/GAP_ANALYSIS.md)하고, 최우선 갭 3개를
스키마+시드+런타임서비스+화면으로 메웠다.

메운 갭 (실제 매입사 핵심 메커니즘)
1. 수수료 3층 분해: 기존 flat MDR → interchange(발급사)+scheme fee(카드망)+
   markup(매입사 수익). 실측 결과 interchange=MDR의 86% (리서치 "70~90%"와 일치).
2. 클리어링 ≠ 정산 분리: 승인·정산과 별도인 프레젠트먼트(클리어링) 단계 추가.
3. 발급사↔매입사 순액정산: BIN/ICA 기준 다자 순액(한국 BC카드 컨소시엄형).
4. 펀딩 항등식: 총매출−환불−채그백−수수료−리저브=입금 (2,924건 전부 항등 성립).
5. 채그백/분쟁 라이프사이클: FIRST_CB→REPRESENT→PRE_ARB→ARBITRATION 상태기계.

스키마 (db/schema.d/70-econ.sql, 96-econ-seed.sql)
- 신규 9테이블: st_interchange_rate, st_scheme_fee, st_fee_component, cl_presentment,
  st_member_bank, st_net_settlement, st_reserve, st_merchant_funding, ac_dispute.
- 기존 정산건 소급 분해(요율표 카드종류×채널 차등) + 순액정산 배치 + 펀딩 계산.
- 시드 계산검증: 3층 항등식·펀딩 항등식 100% 성립(내가 채그백 일수중복 버그 1건 수정).

런타임 서비스 (전부 게이트웨이로 실호출 검증)
- st: ST_FEEDECOMP(ic+sf+markup=mdr), ST_NETSETTLE(BIN별 순액), ST_FUNDING(항등식)
- ac: ACQ_DISPUTE_OPEN, ACQ_DISPUTE_ADVANCE(상태기계-부적법 전이 거부),
  ACQ_PRESENT(클리어링). 각 120줄+, ECPG/XA 격리 규율 준수.

화면 (포털 AR0003 "정산 경제", #econ)
- 수수료 3층 색상바 + 펀딩 항등식 워터폴 + 발급사별 순액정산 + 분쟁 라이프사이클
  + 클리어링. 전부 실측 데이터 조회.

검증
- 통합 빌드 error 0, 도메인 재기동 5,879 서비스 AVAIL
- 6개 신규 서비스 실호출 정상(수수료 3층 84=74+6+4, 상태기계 부적법 전이 거부)
- 기존 6단 매입 체인 COMMIT OK 유지, 미결 2PC 0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:03:35 +09:00

161 lines
10 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ===========================================================================
-- 96-econ-seed.sql : 매입사 경제모델 시드 (70-econ.sql DDL 이후, 실측풍)
-- 요율표(interchange/scheme) + 회원사 은행 + 기존 정산건 소급 분해/클리어링/
-- 순액정산/펀딩. 규제 관행 반영: interchange 가 MDR 의 대부분(70~90%),
-- scheme fee 소액, markup 이 매입사 협상마진.
-- ===========================================================================
-- ---------------------------------------------------------------------------
-- 1. Interchange 요율표 — 카드종류×채널 차등 (interchange qualification)
-- 실제 규제 관행: 체크카드 < 신용카드, 대면(POS) < 비대면(EDI)/해외(FOREIGN).
-- ---------------------------------------------------------------------------
INSERT INTO st_interchange_rate (card_type, mcc, channel, txn_type, rate_bps, fixed_fee) VALUES
('CREDIT','DEFAULT','POS','SALE', 130, 0),
('CREDIT','DEFAULT','EDC','SALE', 140, 0),
('CREDIT','DEFAULT','EDI','SALE', 160, 0), -- 비대면 가산
('CREDIT','DEFAULT','FOREIGN','SALE', 180, 0), -- 해외 가산
('CHECK','DEFAULT','POS','SALE', 70, 0), -- 체크카드 저율
('CHECK','DEFAULT','EDC','SALE', 80, 0),
('CHECK','DEFAULT','EDI','SALE', 95, 0),
('CHECK','DEFAULT','FOREIGN','SALE', 110, 0),
('PREPAID','DEFAULT','POS','SALE', 60, 0),
('CREDIT','DEFAULT','DEFAULT','REFUND', 0, 0), -- 환불은 interchange 역방향(발급사→매입사), 여기선 0 처리
('CREDIT','DEFAULT','DEFAULT','CASH', 100, 0)
ON CONFLICT (card_type, mcc, channel, txn_type) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 2. Scheme fee — 스킴 assessment (소액). BC=국내 표준.
-- ---------------------------------------------------------------------------
INSERT INTO st_scheme_fee (scheme, txn_type, rate_bps, fixed_fee) VALUES
('BC','SALE', 11, 0),
('VISA','SALE', 14, 0),
('MASTER','SALE',13, 0),
('LOCAL','SALE', 9, 0),
('BC','REFUND', 0, 0),
('BC','CASH', 8, 0)
ON CONFLICT (scheme, txn_type) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 3. 회원사 은행 — 발급사↔매입사 다자 순액정산 참가자 (BC카드 컨소시엄형).
-- ---------------------------------------------------------------------------
INSERT INTO st_member_bank (bank_code, bank_name, bin, role) VALUES
('BC', 'BC카드(매입)', '940020','ACQUIRER'),
('KB', 'KB국민카드', '940021','ISSUER'),
('SHINHAN','신한카드', '940022','ISSUER'),
('HYUNDAI','현대카드', '940023','ISSUER'),
('SAMSUNG','삼성카드', '940024','ISSUER'),
('LOTTE', '롯데카드', '940025','ISSUER'),
('NH', 'NH농협카드', '940026','ISSUER'),
('HANA', '하나카드', '940027','ISSUER')
ON CONFLICT (bank_code) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 4. 기존 정산완료 매입건 → 수수료 3층 분해 (소급).
-- 카드종류는 issuer 로 근사(BC=CHECK 30%, 그 외 CREDIT), 채널=purchase.channel.
-- interchange = amount×ic_bps, scheme = amount×scheme_bps,
-- markup = 기존 flat fee interchange scheme (음수면 0 clamp),
-- mdr_total = interchange+scheme+markup (기존 fee 와 근사 일치).
-- ---------------------------------------------------------------------------
INSERT INTO st_fee_component (purchase_id, amount, interchange, scheme_fee, markup, mdr_total, card_type, scheme)
SELECT p.purchase_id, p.amount,
(p.amount * ic.rate_bps / 10000) AS interchange,
(p.amount * sf.rate_bps / 10000) AS scheme_fee,
GREATEST(p.fee - (p.amount*ic.rate_bps/10000) - (p.amount*sf.rate_bps/10000), p.amount*8/10000) AS markup,
(p.amount*ic.rate_bps/10000) + (p.amount*sf.rate_bps/10000)
+ GREATEST(p.fee - (p.amount*ic.rate_bps/10000) - (p.amount*sf.rate_bps/10000), p.amount*8/10000) AS mdr_total,
ct.card_type,
CASE WHEN p.issuer IN ('BC') THEN 'BC' ELSE 'BC' END AS scheme
FROM purchase p
CROSS JOIN LATERAL (SELECT CASE WHEN (p.purchase_id % 10) < 3 THEN 'CHECK' ELSE 'CREDIT' END AS card_type) ct
JOIN st_interchange_rate ic
ON ic.card_type = ct.card_type AND ic.channel = p.channel AND ic.txn_type = 'SALE'
JOIN st_scheme_fee sf ON sf.scheme = 'BC' AND sf.txn_type = 'SALE'
WHERE p.status = 'S'
ON CONFLICT (purchase_id) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 5. 프레젠트먼트(클리어링) — 정산완료건마다 1차 프레젠트먼트 기록.
-- clearing_amt = amount interchange (발급사 귀속분 차감 후 매입사 클리어링액).
-- ---------------------------------------------------------------------------
INSERT INTO cl_presentment (presentment_id, purchase_id, scheme, msg_class, clearing_amt, present_date, cycle, status)
SELECT nextval('cl_present_seq'), fc.purchase_id, fc.scheme, 'FIRST_PRESENT',
fc.amount - fc.interchange, p.biz_date, 'T+1', 'CLEARED'
FROM st_fee_component fc JOIN purchase p ON p.purchase_id = fc.purchase_id
WHERE NOT EXISTS (SELECT 1 FROM cl_presentment c
WHERE c.purchase_id = fc.purchase_id AND c.msg_class = 'FIRST_PRESENT');
-- ---------------------------------------------------------------------------
-- 6. 순액정산 배치 — 발급사(회원 BIN)×영업일 기준 차대변 합산 → 단일 순액.
-- 매입사(BC)가 각 발급사로부터 (매출interchangescheme) 순액을 수취.
-- ---------------------------------------------------------------------------
INSERT INTO st_net_settlement (net_id, scheme, member_bin, biz_date, txn_count,
gross_amt, interchange_amt, scheme_fee_amt, net_amt, direction, status)
SELECT nextval('st_netset_seq'), 'BC', mb.bin, p.biz_date, count(*),
sum(fc.amount), sum(fc.interchange), sum(fc.scheme_fee),
sum(fc.amount - fc.interchange - fc.scheme_fee), 'RECEIVE', 'SETTLED'
FROM st_fee_component fc
JOIN purchase p ON p.purchase_id = fc.purchase_id
JOIN st_member_bank mb ON mb.bank_code = p.issuer
GROUP BY mb.bin, p.biz_date
ON CONFLICT (scheme, member_bin, biz_date) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 7. 롤링 리저브 — 위험업종(HOLD/PENDING 상태였던 가맹점 등) 8% 유보, 그 외 0.
-- 여기선 전 가맹점 일괄 저율(총매출의 0.5%) 유보 예시 + T+30 해제.
-- ---------------------------------------------------------------------------
INSERT INTO st_reserve (merchant_id, biz_date, reserve_bps, reserve_amt, release_date, released)
SELECT p.merchant_id, p.biz_date, 50, sum(p.amount)*50/10000,
p.biz_date + 30, (p.biz_date + 30 < current_date)
FROM purchase p WHERE p.status='S'
GROUP BY p.merchant_id, p.biz_date
ON CONFLICT (merchant_id, biz_date) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 8. 가맹점 펀딩 항등식: 총매출 환불 채그백 수수료 리저브 = 입금.
-- 환불=취소(C)건 금액, 채그백=아래 8단계 시드분, 수수료=MDR합계.
-- ---------------------------------------------------------------------------
INSERT INTO st_merchant_funding (funding_id, merchant_id, biz_date, gross_sales, refunds,
chargebacks, fees, reserve, deposit, funding_model, status)
SELECT nextval('st_funding_seq'), s.merchant_id, s.biz_date,
s.gross, s.refunds, s.cb, s.fees, coalesce(rv.reserve_amt,0),
s.gross - s.refunds - s.cb - s.fees - coalesce(rv.reserve_amt,0),
'NET_DAILY', 'FUNDED'
FROM (
SELECT p.merchant_id, p.biz_date,
sum(CASE WHEN p.status='S' THEN p.amount ELSE 0 END) AS gross,
sum(CASE WHEN p.status='C' THEN p.amount ELSE 0 END) AS refunds,
0::bigint AS cb,
coalesce(sum(fc.mdr_total),0) AS fees
FROM purchase p LEFT JOIN st_fee_component fc ON fc.purchase_id=p.purchase_id
GROUP BY p.merchant_id, p.biz_date
) s
LEFT JOIN st_reserve rv ON rv.merchant_id=s.merchant_id AND rv.biz_date=s.biz_date
WHERE s.gross > 0
ON CONFLICT (merchant_id, biz_date) DO NOTHING;
-- ---------------------------------------------------------------------------
-- 9. 채그백/분쟁 케이스 — 취소·보류였던 매입 일부를 분쟁으로 (reason code 체계).
-- 라이프사이클 단계 혼재: FIRST_CB / REPRESENT / PRE_ARB / ARBITRATION.
-- ---------------------------------------------------------------------------
INSERT INTO ac_dispute (dispute_id, purchase_id, merchant_id, reason_code, amount, stage, status, opened_at, resolved_at, memo)
SELECT nextval('ac_dispute_seq'), p.purchase_id, p.merchant_id,
(ARRAY['10.4','13.1','12.6','13.7','4853'])[1 + (p.purchase_id % 5)],
p.amount,
(ARRAY['FIRST_CB','REPRESENT','PRE_ARB','ARBITRATION'])[1 + (p.purchase_id % 4)],
(ARRAY['OPEN','WON','LOST','ACCEPTED'])[1 + (p.purchase_id % 4)],
p.biz_date::timestamptz + interval '2 days',
CASE WHEN (p.purchase_id % 4) IN (1,2,3) THEN p.biz_date::timestamptz + interval '20 days' ELSE NULL END,
'분쟁 시드: reason ' || (ARRAY['사기','상품미수취','중복청구','품질불량','거래취소'])[1 + (p.purchase_id % 5)]
FROM purchase p
WHERE p.status IN ('C','H') AND (p.purchase_id % 3) = 0
AND NOT EXISTS (SELECT 1 FROM ac_dispute d WHERE d.purchase_id = p.purchase_id);
-- 펀딩의 chargebacks 컬럼을 실제 분쟁(LOST 확정분)으로 보정.
-- 채그백은 원매입의 영업일(biz_date)에 귀속시켜 가맹점×일자 단위로만 차감한다
-- (가맹점 전체 합을 매일 중복 차감하면 입금액이 일수만큼 붕괴한다).
UPDATE st_merchant_funding f SET chargebacks = d.cb,
deposit = f.gross_sales - f.refunds - d.cb - f.fees - f.reserve
FROM (SELECT p.merchant_id, p.biz_date, sum(dp.amount) cb
FROM ac_dispute dp JOIN purchase p ON p.purchase_id = dp.purchase_id
WHERE dp.status = 'LOST' GROUP BY p.merchant_id, p.biz_date) d
WHERE f.merchant_id = d.merchant_id AND f.biz_date = d.biz_date;