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>
This commit is contained in:
parent
46ff41b633
commit
438ae8992d
20 changed files with 1500 additions and 4 deletions
|
|
@ -151,6 +151,10 @@
|
|||
#include "ST_RELAYSPLIT.h"
|
||||
#include "ST_GRADESTAT.h"
|
||||
#include "ST_MONTHSTAT.h"
|
||||
/* Wave G — 매입사 경제모델(수수료 3층 분해/순액정산/펀딩 항등식) */
|
||||
#include "ST_FEEDECOMP.h"
|
||||
#include "ST_NETSETTLE.h"
|
||||
#include "ST_FUNDING.h"
|
||||
|
||||
/* ----- advertise table: {service name, function} ---------------------- */
|
||||
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
|
||||
|
|
@ -221,6 +225,8 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
|
|||
{"ST_DISPUTERSV", ST_DISPUTERSV}, {"ST_APPEALSTAT", ST_APPEALSTAT},
|
||||
{"ST_VANDETAIL", ST_VANDETAIL}, {"ST_RELAYSPLIT", ST_RELAYSPLIT},
|
||||
{"ST_GRADESTAT", ST_GRADESTAT}, {"ST_MONTHSTAT", ST_MONTHSTAT},
|
||||
{"ST_FEEDECOMP", ST_FEEDECOMP}, {"ST_NETSETTLE", ST_NETSETTLE},
|
||||
{"ST_FUNDING", ST_FUNDING},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
26
app/src/st/svc/ST_FEEDECOMP.h
Normal file
26
app/src/st/svc/ST_FEEDECOMP.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* ST_FEEDECOMP.h - st 서비스 ST_FEEDECOMP 카피북 (copybook / record header).
|
||||
* 수수료 3층 분해: MDR = interchange(발급사) + scheme_fee(카드망) + markup(매입사).
|
||||
* st_interchange_rate / st_scheme_fee 요율 조회 후 st_fee_component 로 UPSERT.
|
||||
*/
|
||||
#ifndef ST_FEEDECOMP_H
|
||||
#define ST_FEEDECOMP_H
|
||||
|
||||
#include "st_svc.h"
|
||||
|
||||
typedef struct {
|
||||
long purchase_id;
|
||||
long amount;
|
||||
long interchange; /* 발급사 귀속 */
|
||||
long scheme_fee; /* 카드망 귀속 */
|
||||
long markup; /* 매입사 수익 */
|
||||
long mdr_total; /* = interchange + scheme_fee + markup */
|
||||
char card_type[16]; /* CREDIT/CHECK/PREPAID */
|
||||
char scheme[16]; /* BC/VISA/MASTER/LOCAL */
|
||||
char channel[16];
|
||||
char biz_date[16];
|
||||
} st_st_feedecomp_rec_t;
|
||||
|
||||
void ST_FEEDECOMP(TPSVCINFO *p);
|
||||
|
||||
#endif /* ST_FEEDECOMP_H */
|
||||
156
app/src/st/svc/ST_FEEDECOMP.pgc
Normal file
156
app/src/st/svc/ST_FEEDECOMP.pgc
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* ST_FEEDECOMP.pgc - st (정산/수수료) 모듈 서비스 ST_FEEDECOMP (one service = one file).
|
||||
* 수수료 3층 분해(심화판): MDR = interchange(발급사) + scheme_fee(카드망) + markup(매입사).
|
||||
* 처리단계: 입력검증 -> 매입/카드종류 결정 -> interchange/scheme 요율조회 -> 업무규칙(3층 산출)
|
||||
* -> st_fee_component UPSERT -> 감사이력 -> 재검증 -> 응답 -> 로깅.
|
||||
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). st_svr 가 tpsvrinit 에서 광고.
|
||||
* XA 격리: 체인 신규매입(미커밋)은 이 브랜치에서 안 보이므로 sqlcode=100 을 정상흐름으로 보고
|
||||
* 요청 전문(UBF)값을 사용한다(owner-writes 규율, RECONCILE.pgc 참고). 100 을 FAIL 로 처리하지 않는다.
|
||||
*/
|
||||
#include "ST_FEEDECOMP.h"
|
||||
|
||||
/* ST_FEEDECOMP - 수수료 3층 분해 (interchange + scheme_fee + markup). */
|
||||
void ST_FEEDECOMP(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
|
||||
char channel[16], issuer[40], ctype[16], scheme[16], msgbuf[200];
|
||||
long amount, feein, persist;
|
||||
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid, h_amount, h_fee, h_icbps, h_icfix, h_sfbps, h_sffix;
|
||||
long h_ic, h_sf, h_markup, h_mdr, h_floor, h_cnt;
|
||||
char h_ctype[16], h_scheme[16], h_channel[16], h_issuer[40];
|
||||
char h_merch[64], h_status[16], h_bizdate[16], h_svc[24], h_msg[200];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
strcpy(h_svc, "ST_FEEDECOMP");
|
||||
h_amount = h_fee = h_ic = h_sf = h_markup = h_mdr = h_cnt = 0;
|
||||
h_icbps = 0; h_icfix = 0; h_sfbps = 0; h_sffix = 0; h_floor = 0;
|
||||
strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_merch, "-");
|
||||
strcpy(h_status, "S"); strcpy(h_bizdate, "2026-07-20");
|
||||
persist = 0;
|
||||
|
||||
/* ===== 1) 입력 검증부 ===== */
|
||||
h_pid = getl(b, T_PURCHASE_ID);
|
||||
amount = getl(b, T_AMOUNT);
|
||||
feein = getl(b, T_FEE);
|
||||
gets_(b, T_CHANNEL, channel, sizeof(channel));
|
||||
gets_(b, T_ISSUER, issuer, sizeof(issuer));
|
||||
gets_(b, T_STR1, ctype, sizeof(ctype)); /* 선택: 명시 카드종류 */
|
||||
gets_(b, T_STR2, scheme, sizeof(scheme)); /* 선택: 명시 스킴 */
|
||||
if (channel[0] == 0) strcpy(channel, "POS");
|
||||
if (issuer[0] == 0) strcpy(issuer, "UNKNOWN");
|
||||
userlog("[정산심화] 수수료 3층 분해 진입 pid=%ld amt=%ld chan=%s issuer=%s", h_pid, amount, channel, issuer);
|
||||
if (h_pid <= 0 && amount <= 0) {
|
||||
setl(b, T_RC, 8101); Bchg(b, T_MSG, 0, "매입 ID 또는 (금액+채널) 필수", 0L);
|
||||
userlog("[ST_FEEDECOMP] 입력 부족 pid=%ld amt=%ld", h_pid, amount); FAIL(b);
|
||||
}
|
||||
if (amount < 0) { setl(b, T_RC, 8102); Bchg(b, T_MSG, 0, "매입 금액 음수 불가", 0L); FAIL(b); }
|
||||
|
||||
/* ===== 2) 매입 조회부(미커밋이면 전문값) + 카드종류/스킴 결정 ===== */
|
||||
if (h_pid > 0) {
|
||||
EXEC SQL SELECT amount, fee, channel, issuer, merchant_id, status, biz_date
|
||||
INTO :h_amount, :h_fee, :h_channel, :h_issuer, :h_merch, :h_status, :h_bizdate
|
||||
FROM purchase WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode == 100) {
|
||||
h_amount = amount; h_fee = feein;
|
||||
strncpy(h_channel, channel, sizeof(h_channel)-1); h_channel[sizeof(h_channel)-1] = 0;
|
||||
strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0;
|
||||
userlog("[ST_FEEDECOMP] 체인 신규매입(미커밋) pid=%ld — 전문값으로 분해", h_pid);
|
||||
} else if (sqlca.sqlcode < 0) {
|
||||
setl(b, T_RC, 8110); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L);
|
||||
userlog("[ST_FEEDECOMP] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b);
|
||||
}
|
||||
} else {
|
||||
h_amount = amount; h_fee = feein;
|
||||
strncpy(h_channel, channel, sizeof(h_channel)-1); h_channel[sizeof(h_channel)-1] = 0;
|
||||
strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0;
|
||||
}
|
||||
if (h_amount <= 0) { setl(b, T_RC, 8103); Bchg(b, T_MSG, 0, "분해 대상 금액이 0 이하", 0L); FAIL(b); }
|
||||
/* 카드종류: 명시값 우선, 없으면 매입ID 규칙(하위 3자리 % 10 < 3 → 체크카드) */
|
||||
if (ctype[0] == 0) strcpy(ctype, (h_pid > 0 && (h_pid % 10) < 3) ? "CHECK" : "CREDIT");
|
||||
if (scheme[0] == 0) strcpy(scheme, "BC");
|
||||
strncpy(h_ctype, ctype, sizeof(h_ctype)-1); h_ctype[sizeof(h_ctype)-1] = 0;
|
||||
strncpy(h_scheme, scheme, sizeof(h_scheme)-1); h_scheme[sizeof(h_scheme)-1] = 0;
|
||||
|
||||
/* ===== 3) interchange 요율 조회부 (card_type×channel×SALE) ===== */
|
||||
EXEC SQL SELECT rate_bps, fixed_fee INTO :h_icbps, :h_icfix FROM st_interchange_rate
|
||||
WHERE card_type = :h_ctype AND mcc = 'DEFAULT' AND channel = :h_channel AND txn_type = 'SALE';
|
||||
if (sqlca.sqlcode == 100) {
|
||||
/* 채널 미매핑 → DEFAULT 채널 요율로 폴백 */
|
||||
EXEC SQL SELECT rate_bps, fixed_fee INTO :h_icbps, :h_icfix FROM st_interchange_rate
|
||||
WHERE card_type = :h_ctype AND mcc = 'DEFAULT' AND channel = 'DEFAULT' AND txn_type = 'SALE';
|
||||
if (sqlca.sqlcode == 100) { h_icbps = 130; h_icfix = 0;
|
||||
userlog("[ST_FEEDECOMP] interchange 요율 미존재 ctype=%s chan=%s — 기본 130bp 적용", h_ctype, h_channel); }
|
||||
else if (sqlca.sqlcode < 0) { setl(b, T_RC, 8120); Bchg(b, T_MSG, 0, "interchange 요율 조회 오류", 0L); FAIL(b); }
|
||||
} else if (sqlca.sqlcode < 0) { setl(b, T_RC, 8120); Bchg(b, T_MSG, 0, "interchange 요율 조회 오류", 0L); FAIL(b); }
|
||||
|
||||
/* ===== 4) scheme fee 요율 조회부 (scheme×SALE) ===== */
|
||||
EXEC SQL SELECT rate_bps, fixed_fee INTO :h_sfbps, :h_sffix FROM st_scheme_fee
|
||||
WHERE scheme = :h_scheme AND txn_type = 'SALE';
|
||||
if (sqlca.sqlcode == 100) { h_sfbps = 11; h_sffix = 0;
|
||||
userlog("[ST_FEEDECOMP] scheme 요율 미존재 scheme=%s — 기본 11bp 적용", h_scheme); }
|
||||
else if (sqlca.sqlcode < 0) { setl(b, T_RC, 8130); Bchg(b, T_MSG, 0, "scheme 요율 조회 오류", 0L); FAIL(b); }
|
||||
|
||||
/* ===== 5) 업무규칙 분기부 (3층 산출) ===== */
|
||||
h_ic = h_amount * h_icbps / 10000 + h_icfix; /* 발급사 귀속 */
|
||||
h_sf = h_amount * h_sfbps / 10000 + h_sffix; /* 카드망 귀속 */
|
||||
h_floor = h_amount * 8 / 10000; /* 매입사 최소 마진 8bp */
|
||||
h_markup = h_fee - h_ic - h_sf; /* 기존 flat fee 잔여 = markup */
|
||||
if (h_markup < h_floor) {
|
||||
h_markup = h_floor;
|
||||
userlog("[ST_FEEDECOMP] markup 하한(8bp) 적용 fee=%ld ic=%ld sf=%ld floor=%ld", h_fee, h_ic, h_sf, h_floor);
|
||||
}
|
||||
h_mdr = h_ic + h_sf + h_markup; /* MDR 합계 */
|
||||
userlog("[ST_FEEDECOMP] 3층분해 ic=%ld(%ldbp) sf=%ld(%ldbp) markup=%ld mdr=%ld",
|
||||
h_ic, h_icbps, h_sf, h_sfbps, h_markup, h_mdr);
|
||||
|
||||
/* ===== 6) 본처리부 (st_fee_component UPSERT) — 매입ID 있을 때만 영속 ===== */
|
||||
if (h_pid > 0) {
|
||||
EXEC SQL INSERT INTO st_fee_component
|
||||
(purchase_id, amount, interchange, scheme_fee, markup, mdr_total, card_type, scheme)
|
||||
VALUES (:h_pid, :h_amount, :h_ic, :h_sf, :h_markup, :h_mdr, :h_ctype, :h_scheme)
|
||||
ON CONFLICT (purchase_id) DO UPDATE SET
|
||||
amount = excluded.amount, interchange = excluded.interchange,
|
||||
scheme_fee = excluded.scheme_fee, markup = excluded.markup,
|
||||
mdr_total = excluded.mdr_total, card_type = excluded.card_type,
|
||||
scheme = excluded.scheme, computed_at = now();
|
||||
if (sqlca.sqlcode < 0) {
|
||||
setl(b, T_RC, 8140); Bchg(b, T_MSG, 0, "수수료 분해 UPSERT 실패", 0L);
|
||||
userlog("[ST_FEEDECOMP] st_fee_component UPSERT 실패 [%d]", sqlca.sqlcode); FAIL(b);
|
||||
}
|
||||
persist = 1;
|
||||
} else {
|
||||
userlog("[ST_FEEDECOMP] 매입ID 없음 — 미리보기(preview) 계산만 수행(영속 생략)");
|
||||
}
|
||||
|
||||
/* ===== 7) 감사이력 적재부 (cm_log) + 재검증 ===== */
|
||||
snprintf(msgbuf, sizeof(msgbuf), "ST_FEEDECOMP pid=%ld amt=%ld ic=%ld sf=%ld markup=%ld mdr=%ld ctype=%s",
|
||||
h_pid, h_amount, h_ic, h_sf, h_markup, h_mdr, h_ctype);
|
||||
strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0;
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_msg, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8150); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); }
|
||||
if (persist) {
|
||||
EXEC SQL SELECT count(*) INTO :h_cnt FROM st_fee_component WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8151); Bchg(b, T_MSG, 0, "분해 재검증 조회 오류", 0L); FAIL(b); }
|
||||
if (h_cnt <= 0) { setl(b, T_RC, 8152); Bchg(b, T_MSG, 0, "분해 미적재(보상 롤백)", 0L); FAIL(b); }
|
||||
}
|
||||
|
||||
/* ===== 8) 응답부 (출력 필드 설정) ===== */
|
||||
setl(b, T_AMT1, h_ic); /* interchange */
|
||||
setl(b, T_AMT2, h_sf); /* scheme_fee */
|
||||
setl(b, T_AMT3, h_markup); /* markup */
|
||||
setl(b, T_FEE, h_mdr); /* mdr_total */
|
||||
Bchg(b, T_STR1, 0, h_ctype, 0L);
|
||||
Bchg(b, T_STR2, 0, h_scheme, 0L);
|
||||
Bchg(b, T_SVCNAME, 0, "ST_FEEDECOMP", 0L);
|
||||
setl(b, T_RC, 0);
|
||||
|
||||
/* ===== 9) 완료 로깅 ===== */
|
||||
userlog("[정산심화] 수수료 3층 분해 완료 rc=0 pid=%ld mdr=%ld persist=%ld", h_pid, h_mdr, persist);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
26
app/src/st/svc/ST_FUNDING.h
Normal file
26
app/src/st/svc/ST_FUNDING.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* ST_FUNDING.h - st 서비스 ST_FUNDING 카피북 (copybook / record header).
|
||||
* 가맹점 펀딩 항등식: 총매출 − 환불 − 채그백 − 수수료 − 리저브 = 입금.
|
||||
* gross(S)/refunds(C)/chargebacks(ac_dispute LOST)/fees(st_fee_component)/reserve(st_reserve)
|
||||
* 조회 후 deposit 산출 → st_merchant_funding UPSERT.
|
||||
*/
|
||||
#ifndef ST_FUNDING_H
|
||||
#define ST_FUNDING_H
|
||||
|
||||
#include "st_svc.h"
|
||||
|
||||
typedef struct {
|
||||
char merchant_id[64];
|
||||
char biz_date[16];
|
||||
long gross_sales;
|
||||
long refunds;
|
||||
long chargebacks;
|
||||
long fees; /* MDR 합계 */
|
||||
long reserve;
|
||||
long deposit; /* 실제 입금액 */
|
||||
char funding_model[16];
|
||||
} st_st_funding_rec_t;
|
||||
|
||||
void ST_FUNDING(TPSVCINFO *p);
|
||||
|
||||
#endif /* ST_FUNDING_H */
|
||||
155
app/src/st/svc/ST_FUNDING.pgc
Normal file
155
app/src/st/svc/ST_FUNDING.pgc
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* ST_FUNDING.pgc - st (정산/수수료) 모듈 서비스 ST_FUNDING (one service = one file).
|
||||
* 가맹점 펀딩 항등식(심화판): 총매출(S) − 환불(C) − 채그백(ac_dispute LOST) − 수수료
|
||||
* (st_fee_component) − 리저브(st_reserve) = 입금(deposit) → st_merchant_funding UPSERT.
|
||||
* 처리단계: 입력검증 -> 가맹점 마스터조회 -> 매출/환불/채그백/수수료/리저브 집계
|
||||
* -> 업무규칙(항등식/음수보정) -> st_merchant_funding UPSERT -> 감사이력 -> 재검증 -> 응답 -> 로깅.
|
||||
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). st_svr 가 tpsvrinit 에서 광고.
|
||||
* XA 격리: 채그백/수수료/리저브는 본 브랜치에서 조회 가능한 커밋행 기준으로 집계한다.
|
||||
*/
|
||||
#include "ST_FUNDING.h"
|
||||
|
||||
/* ST_FUNDING - 가맹점 펀딩 항등식(총매출-환불-채그백-수수료-리저브=입금). */
|
||||
void ST_FUNDING(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
|
||||
char merch[64], bizdate[16], model[16], msgbuf[200];
|
||||
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_mcnt, h_gross, h_refunds, h_cb, h_fees, h_reserve, h_deposit;
|
||||
long h_scnt, h_ccnt, h_mdr, h_limit, h_chk;
|
||||
char h_merch[64], h_bizdate[16], h_model[16], h_mstatus[16], h_svc[24], h_msg[200];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
strcpy(h_svc, "ST_FUNDING");
|
||||
h_mcnt = h_gross = h_refunds = h_cb = h_fees = h_reserve = h_deposit = 0;
|
||||
h_scnt = h_ccnt = h_chk = 0; h_mdr = 25; h_limit = 0;
|
||||
strcpy(h_mstatus, "ACTIVE"); strcpy(h_model, "NET_DAILY");
|
||||
|
||||
/* ===== 1) 입력 검증부 ===== */
|
||||
gets_(b, T_MERCHANT, merch, sizeof(merch));
|
||||
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
|
||||
gets_(b, T_STR1, model, sizeof(model)); /* 선택: 펀딩모델 */
|
||||
if (model[0] == 0) strcpy(model, "NET_DAILY");
|
||||
userlog("[정산심화] 가맹점 펀딩 항등식 진입 merch=%s biz=%s model=%s", merch, bizdate, model);
|
||||
if (merch[0] == 0) {
|
||||
setl(b, T_RC, 8301); Bchg(b, T_MSG, 0, "가맹점 ID(merchant_id) 필수", 0L);
|
||||
userlog("[ST_FUNDING] 가맹점 ID 누락"); FAIL(b);
|
||||
}
|
||||
if (strlen(bizdate) < 8) {
|
||||
setl(b, T_RC, 8302); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L);
|
||||
userlog("[ST_FUNDING] 영업일 검증 실패 val=[%s]", bizdate); FAIL(b);
|
||||
}
|
||||
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
strncpy(h_model, model, sizeof(h_model)-1); h_model[sizeof(h_model)-1] = 0;
|
||||
|
||||
/* ===== 2) 가맹점 마스터 조회부 ===== */
|
||||
EXEC SQL SELECT mdr_bps, daily_limit, status
|
||||
INTO :h_mdr, :h_limit, :h_mstatus FROM merchant WHERE merchant_id = :h_merch;
|
||||
if (sqlca.sqlcode == 100) {
|
||||
strcpy(h_mstatus, "NOMERCH"); h_mdr = 25; h_limit = 0;
|
||||
userlog("[ST_FUNDING] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch);
|
||||
} else if (sqlca.sqlcode < 0) {
|
||||
setl(b, T_RC, 8310); Bchg(b, T_MSG, 0, "가맹점 마스터 조회 오류", 0L); FAIL(b);
|
||||
}
|
||||
|
||||
/* ===== 3) 매출/환불/채그백/수수료/리저브 집계부 ===== */
|
||||
/* 총매출(S) + 환불(C) — 상태별 분기 집계 */
|
||||
EXEC SQL SELECT
|
||||
coalesce(sum(CASE WHEN status = 'S' THEN amount ELSE 0 END),0),
|
||||
coalesce(sum(CASE WHEN status = 'C' THEN amount ELSE 0 END),0),
|
||||
coalesce(sum(CASE WHEN status = 'S' THEN 1 ELSE 0 END),0),
|
||||
coalesce(sum(CASE WHEN status = 'C' THEN 1 ELSE 0 END),0),
|
||||
count(*)
|
||||
INTO :h_gross, :h_refunds, :h_scnt, :h_ccnt, :h_mcnt
|
||||
FROM purchase WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8320); Bchg(b, T_MSG, 0, "매출/환불 집계 오류", 0L); FAIL(b); }
|
||||
/* 수수료(MDR 합계) = st_fee_component.mdr_total */
|
||||
EXEC SQL SELECT coalesce(sum(fc.mdr_total),0) INTO :h_fees
|
||||
FROM st_fee_component fc JOIN purchase p ON p.purchase_id = fc.purchase_id
|
||||
WHERE p.merchant_id = :h_merch AND p.biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8321); Bchg(b, T_MSG, 0, "수수료 집계 오류", 0L); FAIL(b); }
|
||||
/* 채그백 = 확정손실(ac_dispute LOST) 금액 */
|
||||
EXEC SQL SELECT coalesce(sum(d.amount),0) INTO :h_cb
|
||||
FROM ac_dispute d JOIN purchase p ON p.purchase_id = d.purchase_id
|
||||
WHERE p.merchant_id = :h_merch AND p.biz_date = :h_bizdate AND d.status = 'LOST';
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8322); Bchg(b, T_MSG, 0, "채그백 집계 오류", 0L); FAIL(b); }
|
||||
/* 리저브 = 당일 유보액(없으면 0) */
|
||||
EXEC SQL SELECT reserve_amt INTO :h_reserve FROM st_reserve
|
||||
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode == 100) { h_reserve = 0;
|
||||
userlog("[ST_FUNDING] 리저브 미설정 mid=%s biz=%s — 0 처리", h_merch, h_bizdate); }
|
||||
else if (sqlca.sqlcode < 0) { setl(b, T_RC, 8323); Bchg(b, T_MSG, 0, "리저브 조회 오류", 0L); FAIL(b); }
|
||||
userlog("[ST_FUNDING] 집계 gross=%ld refunds=%ld cb=%ld fees=%ld reserve=%ld (S=%ld C=%ld)",
|
||||
h_gross, h_refunds, h_cb, h_fees, h_reserve, h_scnt, h_ccnt);
|
||||
|
||||
/* ===== 4) 업무규칙 분기부 (항등식/음수보정) ===== */
|
||||
if (h_gross <= 0) {
|
||||
Bchg(b, T_STATUS, 0, "NODATA", 0L);
|
||||
setl(b, T_AMT1, 0); setl(b, T_AMT2, 0); setl(b, T_AMT3, 0);
|
||||
Bchg(b, T_SVCNAME, 0, "ST_FUNDING", 0L); setl(b, T_RC, 0);
|
||||
userlog("[ST_FUNDING] 당일 매출 없음 merch=%s biz=%s — 무처리 종료", h_merch, h_bizdate);
|
||||
OK(b);
|
||||
}
|
||||
h_deposit = h_gross - h_refunds - h_cb - h_fees - h_reserve; /* 펀딩 항등식 */
|
||||
if (h_deposit < 0)
|
||||
userlog("[ST_FUNDING] 입금액 음수(차감 초과) merch=%s deposit=%ld — 익일 이월 대상", h_merch, h_deposit);
|
||||
|
||||
/* ===== 5) 본처리부 (st_merchant_funding UPSERT) ===== */
|
||||
EXEC SQL INSERT INTO st_merchant_funding
|
||||
(funding_id, merchant_id, biz_date, gross_sales, refunds, chargebacks,
|
||||
fees, reserve, deposit, funding_model, status)
|
||||
VALUES (nextval('st_funding_seq'), :h_merch, :h_bizdate, :h_gross, :h_refunds, :h_cb,
|
||||
:h_fees, :h_reserve, :h_deposit, :h_model, 'FUNDED')
|
||||
ON CONFLICT (merchant_id, biz_date) DO UPDATE SET
|
||||
gross_sales = excluded.gross_sales, refunds = excluded.refunds,
|
||||
chargebacks = excluded.chargebacks, fees = excluded.fees,
|
||||
reserve = excluded.reserve, deposit = excluded.deposit,
|
||||
funding_model = excluded.funding_model, status = 'FUNDED', funded_at = now();
|
||||
if (sqlca.sqlcode < 0) {
|
||||
setl(b, T_RC, 8330); Bchg(b, T_MSG, 0, "펀딩 UPSERT 실패", 0L);
|
||||
userlog("[ST_FUNDING] st_merchant_funding UPSERT 실패 [%d]", sqlca.sqlcode); FAIL(b);
|
||||
}
|
||||
|
||||
/* ===== 6) 감사이력 적재부 (cm_log) ===== */
|
||||
snprintf(msgbuf, sizeof(msgbuf), "ST_FUNDING merch=%s biz=%s gross=%ld fees=%ld cb=%ld deposit=%ld",
|
||||
h_merch, h_bizdate, h_gross, h_fees, h_cb, h_deposit);
|
||||
strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0;
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_msg, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8340); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); }
|
||||
|
||||
/* ===== 7) 재검증부 (항등식 검산 — 적재행 재조회) ===== */
|
||||
EXEC SQL SELECT deposit INTO :h_chk FROM st_merchant_funding
|
||||
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
|
||||
setl(b, T_RC, 8350); Bchg(b, T_MSG, 0, "펀딩 적재 재검증 실패", 0L);
|
||||
userlog("[ST_FUNDING] 재검증 실패 code=%d", sqlca.sqlcode); FAIL(b);
|
||||
}
|
||||
if (h_chk != h_deposit) {
|
||||
setl(b, T_RC, 8351); Bchg(b, T_MSG, 0, "펀딩 항등식 불일치(보상 롤백)", 0L);
|
||||
userlog("[ST_FUNDING] 항등식 불일치 chk=%ld deposit=%ld", h_chk, h_deposit); FAIL(b);
|
||||
}
|
||||
|
||||
/* ===== 8) 응답부 (출력 필드 설정) ===== */
|
||||
setl(b, T_AMT1, h_gross); /* gross */
|
||||
setl(b, T_AMT2, h_fees); /* fees */
|
||||
setl(b, T_AMT3, h_deposit); /* deposit */
|
||||
setl(b, T_AMT4, h_reserve); /* reserve(참고) */
|
||||
setl(b, T_GROSS, h_gross);
|
||||
setl(b, T_FEE, h_fees);
|
||||
setl(b, T_NET, h_deposit);
|
||||
setl(b, T_COUNT, h_mcnt);
|
||||
Bchg(b, T_STATUS, 0, "FUNDED", 0L);
|
||||
Bchg(b, T_SVCNAME, 0, "ST_FUNDING", 0L);
|
||||
setl(b, T_RC, 0);
|
||||
|
||||
/* ===== 9) 완료 로깅 ===== */
|
||||
userlog("[정산심화] 가맹점 펀딩 완료 merch=%s biz=%s gross=%ld fees=%ld deposit=%ld",
|
||||
h_merch, h_bizdate, h_gross, h_fees, h_deposit);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
25
app/src/st/svc/ST_NETSETTLE.h
Normal file
25
app/src/st/svc/ST_NETSETTLE.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* ST_NETSETTLE.h - st 서비스 ST_NETSETTLE 카피북 (copybook / record header).
|
||||
* 발급사(회원 BIN)별 순액정산 배치. 해당 영업일의 st_fee_component+purchase 를
|
||||
* 회원사 BIN 별로 집계(gross/interchange/scheme) → net=gross-ic-sf → st_net_settlement UPSERT.
|
||||
*/
|
||||
#ifndef ST_NETSETTLE_H
|
||||
#define ST_NETSETTLE_H
|
||||
|
||||
#include "st_svc.h"
|
||||
|
||||
typedef struct {
|
||||
char biz_date[16];
|
||||
char issuer[40]; /* 선택: 특정 발급사(bank_code)만 정산 */
|
||||
char member_bin[16];
|
||||
long txn_count;
|
||||
long gross_amt;
|
||||
long interchange_amt;
|
||||
long scheme_fee_amt;
|
||||
long net_amt;
|
||||
char direction[16]; /* RECEIVE/PAY */
|
||||
} st_st_netsettle_rec_t;
|
||||
|
||||
void ST_NETSETTLE(TPSVCINFO *p);
|
||||
|
||||
#endif /* ST_NETSETTLE_H */
|
||||
144
app/src/st/svc/ST_NETSETTLE.pgc
Normal file
144
app/src/st/svc/ST_NETSETTLE.pgc
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* ST_NETSETTLE.pgc - st (정산/수수료) 모듈 서비스 ST_NETSETTLE (one service = one file).
|
||||
* 발급사(회원 BIN)별 순액정산 배치(심화판). 해당 영업일의 st_fee_component+purchase 를
|
||||
* 회원사(st_member_bank) BIN 별로 집계 → gross/interchange/scheme 합산, net=gross-ic-sf
|
||||
* → st_net_settlement UPSERT(ON CONFLICT scheme,member_bin,biz_date).
|
||||
* 처리단계: 입력검증 -> 참가회원사/대상건 조회 -> 업무규칙(대상유무) -> 순액배치 UPSERT(집합기반)
|
||||
* -> 결과 커서순회 로깅 -> 감사이력 -> 총계 재집계 -> 응답 -> 로깅.
|
||||
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). st_svr 가 tpsvrinit 에서 광고.
|
||||
* 쓰기 소유자 단일화(owner-writes): 순액배치 UPSERT 는 집합기반 단일 문장으로 수행한다.
|
||||
*/
|
||||
#include "ST_NETSETTLE.h"
|
||||
|
||||
/* ST_NETSETTLE - 발급사별 순액정산 배치. */
|
||||
void ST_NETSETTLE(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
|
||||
char bizdate[16], issuer[40], msgbuf[200];
|
||||
long rows = 0;
|
||||
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_members, h_srccnt, h_upcnt, h_tcnt, h_tgross, h_tic, h_tsf, h_tnet;
|
||||
long h_rcnt, h_rgross, h_rnet;
|
||||
char h_bizdate[16], h_issuer[40], h_bin[16], h_dir[16], h_svc[24], h_msg[200];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
strcpy(h_svc, "ST_NETSETTLE");
|
||||
h_members = h_srccnt = h_upcnt = 0;
|
||||
h_tcnt = h_tgross = h_tic = h_tsf = h_tnet = 0;
|
||||
h_rcnt = h_rgross = h_rnet = 0;
|
||||
strcpy(h_bin, "-"); strcpy(h_dir, "RECEIVE");
|
||||
|
||||
/* ===== 1) 입력 검증부 ===== */
|
||||
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
|
||||
gets_(b, T_ISSUER, issuer, sizeof(issuer)); /* 선택: 특정 발급사만 */
|
||||
if (strlen(bizdate) < 8) {
|
||||
setl(b, T_RC, 8201); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L);
|
||||
userlog("[ST_NETSETTLE] 영업일 검증 실패 val=[%s]", bizdate); FAIL(b);
|
||||
}
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0;
|
||||
userlog("[정산심화] 발급사별 순액정산 배치 진입 biz=%s issuer=[%s]", h_bizdate, h_issuer);
|
||||
|
||||
/* ===== 2) 참가 회원사/대상건 조회부 ===== */
|
||||
EXEC SQL SELECT count(*) INTO :h_members FROM st_member_bank
|
||||
WHERE role IN ('ISSUER', 'BOTH') AND (:h_issuer = '' OR bank_code = :h_issuer);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8210); Bchg(b, T_MSG, 0, "회원사 조회 오류", 0L); FAIL(b); }
|
||||
EXEC SQL SELECT count(*) INTO :h_srccnt
|
||||
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
|
||||
WHERE p.biz_date = :h_bizdate AND (:h_issuer = '' OR p.issuer = :h_issuer);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8211); Bchg(b, T_MSG, 0, "정산 대상 조회 오류", 0L); FAIL(b); }
|
||||
userlog("[ST_NETSETTLE] 참가 회원사=%ld 대상 분해건=%ld", h_members, h_srccnt);
|
||||
|
||||
/* ===== 3) 업무규칙 분기부 (대상 유무) ===== */
|
||||
if (h_srccnt <= 0) {
|
||||
Bchg(b, T_STATUS, 0, "NODATA", 0L);
|
||||
setl(b, T_ARG1, 0); setl(b, T_AMT1, 0); setl(b, T_AMT2, 0);
|
||||
Bchg(b, T_SVCNAME, 0, "ST_NETSETTLE", 0L); setl(b, T_RC, 0);
|
||||
userlog("[ST_NETSETTLE] 대상 없음 biz=%s — 무처리 종료", h_bizdate);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* ===== 4) 본처리부: 발급사 BIN×영업일 순액배치 UPSERT (집합기반 단일문장) =====
|
||||
* net_amt = gross - interchange - scheme_fee (부호=수취/지급 방향). */
|
||||
EXEC SQL 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(*),
|
||||
coalesce(sum(fc.amount),0), coalesce(sum(fc.interchange),0), coalesce(sum(fc.scheme_fee),0),
|
||||
coalesce(sum(fc.amount - fc.interchange - fc.scheme_fee),0),
|
||||
CASE WHEN coalesce(sum(fc.amount - fc.interchange - fc.scheme_fee),0) >= 0
|
||||
THEN 'RECEIVE' ELSE 'PAY' END,
|
||||
'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
|
||||
WHERE p.biz_date = :h_bizdate AND (:h_issuer = '' OR p.issuer = :h_issuer)
|
||||
GROUP BY mb.bin, p.biz_date
|
||||
ON CONFLICT (scheme, member_bin, biz_date) DO UPDATE SET
|
||||
txn_count = excluded.txn_count, gross_amt = excluded.gross_amt,
|
||||
interchange_amt = excluded.interchange_amt, scheme_fee_amt = excluded.scheme_fee_amt,
|
||||
net_amt = excluded.net_amt, direction = excluded.direction, settled_at = now();
|
||||
if (sqlca.sqlcode < 0) {
|
||||
setl(b, T_RC, 8220); Bchg(b, T_MSG, 0, "순액정산 배치 UPSERT 실패", 0L);
|
||||
userlog("[ST_NETSETTLE] st_net_settlement UPSERT 실패 [%d]", sqlca.sqlcode); FAIL(b);
|
||||
}
|
||||
h_upcnt = sqlca.sqlerrd[2]; /* 처리 행수 */
|
||||
userlog("[ST_NETSETTLE] 순액배치 UPSERT 처리행=%ld", h_upcnt);
|
||||
|
||||
/* ===== 5) 결과 커서 순회 로깅부 (같은 XA 브랜치 — 방금 UPSERT 가시) ===== */
|
||||
EXEC SQL DECLARE cur_netset CURSOR FOR
|
||||
SELECT member_bin, txn_count, gross_amt, net_amt, direction
|
||||
FROM st_net_settlement
|
||||
WHERE biz_date = :h_bizdate
|
||||
AND (:h_issuer = '' OR member_bin IN (SELECT bin FROM st_member_bank WHERE bank_code = :h_issuer))
|
||||
ORDER BY member_bin;
|
||||
EXEC SQL OPEN cur_netset;
|
||||
for (;;) {
|
||||
EXEC SQL FETCH cur_netset INTO :h_bin, :h_rcnt, :h_rgross, :h_rnet, :h_dir;
|
||||
if (sqlca.sqlcode == 100) break;
|
||||
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_netset; setl(b, T_RC, 8230);
|
||||
Bchg(b, T_MSG, 0, "순액정산 결과 순회 오류", 0L); FAIL(b); }
|
||||
rows++;
|
||||
userlog("[ST_NETSETTLE] BIN=%s cnt=%ld gross=%ld net=%ld dir=%s",
|
||||
h_bin, h_rcnt, h_rgross, h_rnet, h_dir);
|
||||
}
|
||||
EXEC SQL CLOSE cur_netset;
|
||||
|
||||
/* ===== 6) 감사이력 적재부 (cm_log) ===== */
|
||||
snprintf(msgbuf, sizeof(msgbuf), "ST_NETSETTLE biz=%s members=%ld src=%ld upsert=%ld rows=%ld",
|
||||
h_bizdate, h_members, h_srccnt, h_upcnt, rows);
|
||||
strncpy(h_msg, msgbuf, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0;
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (nextval('cm_log_seq'), :h_svc, 'I', :h_msg, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8240); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); }
|
||||
|
||||
/* ===== 7) 총계 재집계부 ===== */
|
||||
EXEC SQL SELECT coalesce(sum(txn_count),0), coalesce(sum(gross_amt),0),
|
||||
coalesce(sum(interchange_amt),0), coalesce(sum(scheme_fee_amt),0),
|
||||
coalesce(sum(net_amt),0)
|
||||
INTO :h_tcnt, :h_tgross, :h_tic, :h_tsf, :h_tnet
|
||||
FROM st_net_settlement
|
||||
WHERE biz_date = :h_bizdate
|
||||
AND (:h_issuer = '' OR member_bin IN (SELECT bin FROM st_member_bank WHERE bank_code = :h_issuer));
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8250); Bchg(b, T_MSG, 0, "순액정산 총계 재집계 오류", 0L); FAIL(b); }
|
||||
|
||||
/* ===== 8) 응답부 (출력 필드 설정) ===== */
|
||||
setl(b, T_ARG1, h_tcnt); /* txn_count 총계 */
|
||||
setl(b, T_AMT1, h_tgross); /* gross 총계 */
|
||||
setl(b, T_AMT2, h_tnet); /* net 총계 */
|
||||
setl(b, T_AMT3, h_tic); /* interchange 총계(참고) */
|
||||
setl(b, T_COUNT, rows); /* 정산 BIN 수 */
|
||||
Bchg(b, T_STATUS, 0, "SETTLED", 0L);
|
||||
Bchg(b, T_SVCNAME, 0, "ST_NETSETTLE", 0L);
|
||||
setl(b, T_RC, 0);
|
||||
|
||||
/* ===== 9) 완료 로깅 ===== */
|
||||
userlog("[정산심화] 발급사별 순액정산 완료 biz=%s bins=%ld txn=%ld gross=%ld net=%ld",
|
||||
h_bizdate, rows, h_tcnt, h_tgross, h_tnet);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
Loading…
Add table
Add a link
Reference in a new issue