Wave G2: 경제모델 2·3순위 갭 (MCC interchange 등급·ISO8583 클리어링·이중/단일)

갭분석 2·3순위 보강. 전부 실측 검증.

① interchange qualification 심화 (MCC 업종별 등급)
- mm_merch_mcc: 40개 가맹점 → 실제 4자리 ISO MCC (커피 5814, 마트 5411,
  면세점 5309, 병원 8062, 주유소 5541, 여행사 4722, 숙박 7011 ...)
- mm_mcc: 26개 MCC + risk tier(1 저율/2 표준/3 고율)
- st_interchange_rate 에 MCC×카드종류 요율 override → fee_component 재계산
- 실측: tier1(병원0.69%/약국0.72%/주유0.75%) < tier2(요식~1.15%) < tier3(여행~1.62%)
  — 실제 매입사 interchange 차등 그대로. 3층 항등 7560/7560, 펀딩 2925/2925 유지.

② ISO 8583 클리어링 메시지 클래스 (승인전문과 분리된 클리어링 단계)
- mg_clearing: PRESENTMENT(1240)/CHARGEBACK(1442)/REPRESENT/RECONCILE(1500)/
  FEE_COLLECT(1740). 실측 8,658건.
- MG_CLEARING_INQ 서비스(mg, 120줄): 클래스/영업일/pid 필터 집계 조회.
  실호출 검증(DUAL 6,386 / SINGLE 2,272, 대표 MTI 1240).

③ 이중/단일 메시지 구분
- msg_type: 신용=DUAL(5,288) / 체크=SINGLE(2,272). 실제 dual/single-message 프로토콜.

포털 "정산 경제"(#econ)에 MCC 등급표 + ISO8583 클리어링 메시지표 추가.
통합 빌드 error 0, 도메인 5,883 서비스 AVAIL, 6단 체인 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyeongwoo 2026-07-22 15:18:40 +09:00
parent 438ae8992d
commit 2ac4c3c920
8 changed files with 343 additions and 5 deletions

View file

@ -29,6 +29,7 @@
#include "MG_CONVERT.h"
#include "MG_ERRMSG.h"
#include "MG_LOG.h"
#include "MG_CLEARING_INQ.h"
#include "MG_CHANAUTH.h"
#include "MG_SESSION.h"
#include "MG_ACK.h"
@ -181,6 +182,7 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"MG_STAN", MG_STAN}, {"MG_ROUTE", MG_ROUTE},
{"MG_RESEND", MG_RESEND}, {"MG_CONVERT", MG_CONVERT},
{"MG_ERRMSG", MG_ERRMSG}, {"MG_LOG", MG_LOG},
{"MG_CLEARING_INQ", MG_CLEARING_INQ},
{"MG_CHANAUTH", MG_CHANAUTH}, {"MG_SESSION", MG_SESSION},
{"MG_ACK", MG_ACK}, {"MG_NACK", MG_NACK},
{"MG_ECHO", MG_ECHO}, {"MG_HEALTH", MG_HEALTH},

View file

@ -0,0 +1,12 @@
/*
* MG_CLEARING_INQ.h - mg () MG_CLEARING_INQ .
* ISO 8583 (mg_clearing) . Advertised by mg_svr.
*/
#ifndef MG_CLEARING_INQ_H
#define MG_CLEARING_INQ_H
#include "mg_svc.h"
void MG_CLEARING_INQ(TPSVCINFO *p);
#endif /* MG_CLEARING_INQ_H */

View file

@ -0,0 +1,121 @@
/*
* MG_CLEARING_INQ.pgc - mg 모듈 서비스 MG_CLEARING_INQ (one service = one file).
* ISO 8583 클리어링 메시지(mg_clearing) 조회. 승인전문(mg_msg_log)과 분리된
* 클리어링 단계의 전문 클래스(PRESENTMENT/CHARGEBACK/REPRESENT/RECONCILE/
* FEE_COLLECT/FILE_REJECT)를 집계·조회한다.
* 입력계약:
* T_PURCHASE_ID (선택) 특정 매입건의 클리어링 이력 조회
* T_STR1 (선택) 특정 msg_class 필터
* T_BIZDATE (선택) 영업일
* 출력계약:
* T_COUNT=총 메시지 건수, T_ARG1=DUAL 건수, T_ARG2=SINGLE 건수,
* T_AMT1=총 금액, T_STR2=대표 MTI, T_STATUS=조회상태.
* Runs on caller's XA branch (no EXEC SQL CONNECT). Advertised by mg_svr.
*/
#include "MG_CLEARING_INQ.h"
void MG_CLEARING_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *SVC = "MG_CLEARING_INQ";
char in_class[24], in_bizdate[16];
long in_pid;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_class[24];
char h_bizdate[16];
long h_total, h_dual, h_single, h_amt;
long h_present, h_chargeback, h_represent, h_reconcile, h_feecol, h_reject;
char h_top_mti[8];
EXEC SQL END DECLARE SECTION;
userlog("%s 진입: ISO 8583 클리어링 메시지 조회", SVC);
/* --- 1) 입력 검증/수집부 ------------------------------------------- */
in_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_STR1, in_class, sizeof(in_class));
gets_(b, T_BIZDATE, in_bizdate, sizeof(in_bizdate));
h_pid = in_pid;
strncpy(h_class, in_class, sizeof(h_class) - 1); h_class[sizeof(h_class) - 1] = 0;
strncpy(h_bizdate, in_bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate) - 1] = 0;
if (h_class[0] == 0) strcpy(h_class, "ALL");
if (h_bizdate[0] == 0) strcpy(h_bizdate, "ALL");
h_total = h_dual = h_single = h_amt = 0;
h_present = h_chargeback = h_represent = h_reconcile = h_feecol = h_reject = 0;
strcpy(h_top_mti, "");
/* --- 2) 본 조회부: 필터 조합별 집계 -------------------------------- */
if (h_pid > 0) {
/* 특정 매입건의 클리어링 이력 (라이프사이클 추적) */
EXEC SQL SELECT count(*),
coalesce(sum(CASE WHEN msg_type='DUAL' THEN 1 ELSE 0 END), 0),
coalesce(sum(CASE WHEN msg_type='SINGLE' THEN 1 ELSE 0 END), 0),
coalesce(sum(amount), 0)
INTO :h_total, :h_dual, :h_single, :h_amt
FROM mg_clearing WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
userlog("%s: 매입건 클리어링 조회 실패 [%d]", SVC, sqlca.sqlcode);
setl(b, T_RC, 9501); sets_(b, T_MSG, "클리어링 조회 실패"); FAIL(b);
}
} else {
/* 전체(또는 클래스/영업일 필터) 집계 */
EXEC SQL SELECT count(*),
coalesce(sum(CASE WHEN msg_type='DUAL' THEN 1 ELSE 0 END), 0),
coalesce(sum(CASE WHEN msg_type='SINGLE' THEN 1 ELSE 0 END), 0),
coalesce(sum(amount), 0)
INTO :h_total, :h_dual, :h_single, :h_amt
FROM mg_clearing
WHERE (:h_class = 'ALL' OR msg_class = :h_class)
AND (:h_bizdate = 'ALL' OR to_char(biz_date,'YYYY-MM-DD') = :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("%s: 클리어링 집계 실패 [%d]", SVC, sqlca.sqlcode);
setl(b, T_RC, 9502); sets_(b, T_MSG, "클리어링 집계 실패"); FAIL(b);
}
}
/* --- 3) 메시지 클래스별 분해 (전체 조회일 때만) -------------------- */
if (h_pid <= 0) {
EXEC SQL SELECT
coalesce(sum(CASE WHEN msg_class='PRESENTMENT' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN msg_class='CHARGEBACK' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN msg_class='REPRESENT' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN msg_class='RECONCILE' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN msg_class='FEE_COLLECT' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN status='REJECTED' THEN 1 ELSE 0 END),0)
INTO :h_present, :h_chargeback, :h_represent, :h_reconcile, :h_feecol, :h_reject
FROM mg_clearing
WHERE (:h_bizdate = 'ALL' OR to_char(biz_date,'YYYY-MM-DD') = :h_bizdate);
if (sqlca.sqlcode < 0) { userlog("%s: 클래스 분해 실패 [%d]", SVC, sqlca.sqlcode); }
}
/* --- 4) 대표 MTI 조회 (건수 최다 클래스의 MTI) --------------------- */
EXEC SQL SELECT mti INTO :h_top_mti FROM mg_clearing
WHERE (:h_class = 'ALL' OR msg_class = :h_class)
AND (:h_pid <= 0 OR purchase_id = :h_pid)
GROUP BY mti ORDER BY count(*) DESC LIMIT 1;
if (sqlca.sqlcode == 100) strcpy(h_top_mti, "-");
/* --- 5) 업무규칙: 이상신호 판정(거부 메시지 존재시 경보) ----------- */
if (h_reject > 0)
userlog("%s 경보: 거부(REJECTED) 클리어링 메시지 %ld건", SVC, h_reject);
/* --- 6) 응답 전문 구성부 ------------------------------------------- */
setl(b, T_COUNT, h_total);
setl(b, T_ARG1, h_dual);
setl(b, T_ARG2, h_single);
setl(b, T_AMT1, h_amt);
setl(b, T_AMT2, h_present);
setl(b, T_AMT3, h_chargeback);
setl(b, T_ARG3, h_reconcile);
setl(b, T_ARG4, h_feecol);
sets_(b, T_STR2, h_top_mti);
setl(b, T_RC, 0);
sets_(b, T_STATUS, h_total > 0 ? "FOUND" : "EMPTY");
/* --- 7) 로깅부 ----------------------------------------------------- */
userlog("%s 완료: total=%ld dual=%ld single=%ld amt=%ld present=%ld cb=%ld recon=%ld",
SVC, h_total, h_dual, h_single, h_amt, h_present, h_chargeback, h_reconcile);
OK(b);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
/* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4638,"u":"개"},{"k":"총 코드 라인","v":331721,"u":"LOC"},{"k":"온라인 서비스","v":1469,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23810,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47113,"u":"곳"},{"k":"DB 테이블","v":84,"u":"개"},{"k":"카피북 헤더(.h)","v":2213,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":989,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23810,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47113,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6904,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2253,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2213,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2253,"goto":64,"userlog":17179,"sqlca_checks":18788,"embedded_sql_density":9.8}};
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4640,"u":"개"},{"k":"총 코드 라인","v":331856,"u":"LOC"},{"k":"온라인 서비스","v":1470,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23817,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47127,"u":"곳"},{"k":"DB 테이블","v":86,"u":"개"},{"k":"카피북 헤더(.h)","v":2214,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":989,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23817,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47127,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6906,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2254,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2214,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2254,"goto":64,"userlog":17185,"sqlca_checks":18795,"embedded_sql_density":9.8}};

36
db/schema.d/71-econ2.sql Normal file
View file

@ -0,0 +1,36 @@
-- ===========================================================================
-- 71-econ2.sql : 매입사 경제모델 2차 고도화 (갭분석 2·3순위)
-- ① interchange qualification 심화: MCC(업종)별 요율 tier
-- ② ISO 8583 클리어링 메시지 클래스 (presentment/chargeback/recon/fee/reject)
-- ③ 이중메시지 vs 단일메시지 구분
-- ===========================================================================
-- ---------------------------------------------------------------------------
-- 1. 가맹점 → MCC 매핑 (실제 4자리 ISO MCC).
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_merch_mcc (
merchant_id TEXT PRIMARY KEY,
mcc TEXT NOT NULL
);
-- ---------------------------------------------------------------------------
-- 2. ISO 8583 클리어링 메시지 (승인전문 mg_msg_log 과 별개인 클리어링 단계).
-- msg_class: 실제 ISO 8583 클리어링 기능 (Berlin Group).
-- msg_type : DUAL(신용/서명-승인과 클리어링 분리) / SINGLE(PIN/직불-통합).
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS mg_clearing_seq START 500000;
CREATE TABLE IF NOT EXISTS mg_clearing (
clr_id BIGINT PRIMARY KEY,
purchase_id BIGINT,
scheme TEXT NOT NULL DEFAULT 'BC',
msg_class TEXT NOT NULL, -- PRESENTMENT/CHARGEBACK/REPRESENT/RECONCILE/FEE_COLLECT/FILE_REJECT
mti TEXT NOT NULL, -- 1240(1st present)/1442(chargeback)/1740(fee)/1500(recon)/1644(file action)
msg_type TEXT NOT NULL DEFAULT 'DUAL', -- DUAL / SINGLE
amount BIGINT NOT NULL DEFAULT 0,
biz_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'ACCEPTED', -- ACCEPTED/REJECTED
reject_code TEXT, -- 파일/메시지 거부 사유
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_clearing_pid ON mg_clearing(purchase_id);
CREATE INDEX IF NOT EXISTS ix_clearing_class ON mg_clearing(msg_class);

View file

@ -0,0 +1,157 @@
-- ===========================================================================
-- 97-econ2-seed.sql : 2차 경제모델 시드 (71-econ2 DDL + 96-econ-seed 이후 실행).
-- MCC 부여 → MCC별 interchange tier 로 수수료 재계산 → 순액정산·펀딩 재집계 →
-- ISO 8583 클리어링 메시지 생성. 전부 idempotent(재실행 안전).
-- ===========================================================================
-- ---------------------------------------------------------------------------
-- 1. MCC 마스터 (실제 4자리 ISO MCC + interchange tier).
-- tier(risk_level): 1=저율(생필/주유) 2=표준(요식/소매) 3=고율(여행/유흥).
-- ---------------------------------------------------------------------------
INSERT INTO mm_mcc (mcc, mcc_name, risk_level) VALUES
('5411','슈퍼마켓/식료품',1),('5541','주유소',1),('5912','약국',1),('8062','병원',1),
('5812','일반음식점',2),('5814','패스트푸드/카페',2),('5732','전자제품',2),
('5942','서점',2),('5943','문구',2),('5992','화훼',2),('5712','가구/인테리어',2),
('7216','세탁',2),('8043','안경',2),('0742','동물병원',2),('5571','오토바이',2),
('5309','면세점',2),('5941','스포츠/캠핑용품',2),('7230','미용',2),
('4722','여행사',3),('7011','숙박',3),('7994','게임/PC',3),('7929','노래연습장',3),
('7997','휘트니스/골프',3),('7992','골프연습',3),('7299','예식/서비스',3),('8299','교육',2)
ON CONFLICT (mcc) DO UPDATE SET mcc_name=EXCLUDED.mcc_name, risk_level=EXCLUDED.risk_level;
-- ---------------------------------------------------------------------------
-- 2. 가맹점 → MCC 매핑 (상호 기반).
-- ---------------------------------------------------------------------------
INSERT INTO mm_merch_mcc (merchant_id, mcc) VALUES
('M0001','5814'),('M0002','5411'),('M0003','5309'),('M0004','5812'),('M0005','8062'),
('M0006','7230'),('M0007','5812'),('M0008','5912'),('M0009','5541'),('M0010','5942'),
('M0011','5812'),('M0012','5712'),('M0013','8043'),('M0014','5812'),('M0015','7994'),
('M0016','7997'),('M0017','5814'),('M0018','5732'),('M0019','5992'),('M0020','5411'),
('M0021','7216'),('M0022','5912'),('M0023','7929'),('M0024','0742'),('M0025','7992'),
('M0026','5812'),('M0027','5943'),('M0028','5712'),('M0029','5411'),('M0030','5941'),
('M0031','5571'),('M0032','7299'),('M0033','4722'),('M0034','5812'),('M0035','7011'),
('M0036','8299'),('M0037','7997'),('M0038','5732'),('M0039','5814'),('M0040','5812')
ON CONFLICT (merchant_id) DO UPDATE SET mcc=EXCLUDED.mcc;
-- ---------------------------------------------------------------------------
-- 3. MCC×카드종류 interchange 요율(tier 반영). DEFAULT 채널 기준 override 행.
-- tier1 저율, tier2 표준, tier3 고율. 체크카드는 신용 대비 저율.
-- ---------------------------------------------------------------------------
INSERT INTO st_interchange_rate (card_type, mcc, channel, txn_type, rate_bps, fixed_fee)
SELECT ct, m.mcc, 'DEFAULT', 'SALE',
CASE m.risk_level WHEN 1 THEN 90 WHEN 3 THEN 175 ELSE 130 END
- CASE WHEN ct='CHECK' THEN 55 WHEN ct='PREPAID' THEN 65 ELSE 0 END,
0
FROM mm_mcc m CROSS JOIN (VALUES ('CREDIT'),('CHECK'),('PREPAID')) v(ct)
ON CONFLICT (card_type, mcc, channel, txn_type) DO UPDATE SET rate_bps=EXCLUDED.rate_bps;
-- ---------------------------------------------------------------------------
-- 4. 수수료 재계산 — MCC tier interchange 우선(있으면), 없으면 채널요율 폴백.
-- interchange+scheme_fee+markup=mdr_total 항등 유지, markup 하한 8bp.
-- ---------------------------------------------------------------------------
UPDATE st_fee_component fc SET
interchange = ic.amt,
markup = GREATEST(fc.mdr_total - ic.amt - fc.scheme_fee, fc.amount*8/10000),
mdr_total = ic.amt + fc.scheme_fee
+ GREATEST(fc.mdr_total - ic.amt - fc.scheme_fee, fc.amount*8/10000)
FROM (
SELECT fc2.purchase_id,
fc2.amount * COALESCE(r.rate_bps, fc2.amount*0 + 130) / 10000 AS amt
FROM st_fee_component fc2
JOIN purchase p ON p.purchase_id = fc2.purchase_id
LEFT JOIN mm_merch_mcc mm ON mm.merchant_id = p.merchant_id
LEFT JOIN st_interchange_rate r
ON r.card_type = fc2.card_type AND r.mcc = mm.mcc
AND r.channel = 'DEFAULT' AND r.txn_type = 'SALE'
) ic
WHERE ic.purchase_id = fc.purchase_id;
-- ---------------------------------------------------------------------------
-- 5. 클리어링액(프레젠트먼트) 재동기화 = amount interchange.
-- ---------------------------------------------------------------------------
UPDATE cl_presentment c SET clearing_amt = fc.amount - fc.interchange
FROM st_fee_component fc WHERE fc.purchase_id = c.purchase_id;
-- ---------------------------------------------------------------------------
-- 6. 순액정산 재집계 (fee_component 변경 반영).
-- ---------------------------------------------------------------------------
TRUNCATE st_net_settlement;
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;
-- ---------------------------------------------------------------------------
-- 7. 펀딩 재집계 (수수료 변경 반영).
-- ---------------------------------------------------------------------------
TRUNCATE st_merchant_funding;
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, 0, s.fees, coalesce(rv.reserve_amt,0),
s.gross - s.refunds - 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) gross,
sum(CASE WHEN p.status='C' THEN p.amount ELSE 0 END) refunds,
coalesce(sum(fc.mdr_total),0) 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;
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;
-- ---------------------------------------------------------------------------
-- 8. ISO 8583 클리어링 메시지 — 클리어링 단계의 실제 전문 클래스.
-- PRESENTMENT(1240): 정산완료건. msg_type: 신용=DUAL, 체크=SINGLE.
-- ---------------------------------------------------------------------------
INSERT INTO mg_clearing (clr_id, purchase_id, scheme, msg_class, mti, msg_type, amount, biz_date, status)
SELECT nextval('mg_clearing_seq'), fc.purchase_id, fc.scheme, 'PRESENTMENT', '1240',
CASE WHEN fc.card_type='CHECK' THEN 'SINGLE' ELSE 'DUAL' END,
fc.amount - fc.interchange, p.biz_date, 'ACCEPTED'
FROM st_fee_component fc JOIN purchase p ON p.purchase_id = fc.purchase_id
WHERE NOT EXISTS (SELECT 1 FROM mg_clearing c
WHERE c.purchase_id = fc.purchase_id AND c.msg_class='PRESENTMENT');
-- CHARGEBACK(1442): 분쟁 개시건.
INSERT INTO mg_clearing (clr_id, purchase_id, scheme, msg_class, mti, msg_type, amount, biz_date, status)
SELECT nextval('mg_clearing_seq'), d.purchase_id, 'BC', 'CHARGEBACK', '1442', 'DUAL',
d.amount, d.opened_at::date, 'ACCEPTED'
FROM ac_dispute d
WHERE NOT EXISTS (SELECT 1 FROM mg_clearing c
WHERE c.purchase_id = d.purchase_id AND c.msg_class='CHARGEBACK');
-- REPRESENT(1240 재제출): representment 단계 이상 분쟁.
INSERT INTO mg_clearing (clr_id, purchase_id, scheme, msg_class, mti, msg_type, amount, biz_date, status)
SELECT nextval('mg_clearing_seq'), d.purchase_id, 'BC', 'REPRESENT', '1240', 'DUAL',
d.amount, d.opened_at::date + 5, 'ACCEPTED'
FROM ac_dispute d WHERE d.stage IN ('REPRESENT','PRE_ARB','ARBITRATION')
AND NOT EXISTS (SELECT 1 FROM mg_clearing c
WHERE c.purchase_id = d.purchase_id AND c.msg_class='REPRESENT');
-- RECONCILE(1500): 발급사×영업일 정산대사 메시지 (순액정산 배치당 1건).
INSERT INTO mg_clearing (clr_id, purchase_id, scheme, msg_class, mti, msg_type, amount, biz_date, status)
SELECT nextval('mg_clearing_seq'), NULL, 'BC', 'RECONCILE', '1500', 'DUAL',
ns.net_amt, ns.biz_date, 'ACCEPTED'
FROM st_net_settlement ns
WHERE NOT EXISTS (SELECT 1 FROM mg_clearing c
WHERE c.msg_class='RECONCILE' AND c.biz_date=ns.biz_date AND c.amount=ns.net_amt);
-- FEE_COLLECT(1740): scheme fee 징수 메시지 (일별 집계 1건).
INSERT INTO mg_clearing (clr_id, purchase_id, scheme, msg_class, mti, msg_type, amount, biz_date, status)
SELECT nextval('mg_clearing_seq'), NULL, 'BC', 'FEE_COLLECT', '1740', 'DUAL',
sum(fc.scheme_fee), p.biz_date, 'ACCEPTED'
FROM st_fee_component fc JOIN purchase p ON p.purchase_id=fc.purchase_id
GROUP BY p.biz_date
HAVING NOT EXISTS (SELECT 1 FROM mg_clearing c WHERE c.msg_class='FEE_COLLECT' AND c.biz_date=p.biz_date);