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:
hyeongwoo 2026-07-22 14:03:35 +09:00
parent 46ff41b633
commit 438ae8992d
20 changed files with 1500 additions and 4 deletions

View file

@ -155,6 +155,11 @@
#include "ACQ_RETURNQRY.h"
#include "ACQ_RETURNSTAT.h"
/* Wave G — 매입사 경제모델(분쟁 라이프사이클 + 클리어링) */
#include "ACQ_DISPUTE_OPEN.h"
#include "ACQ_DISPUTE_ADVANCE.h"
#include "ACQ_PRESENT.h"
/* ----- advertise table: {service name, function} (fn is extern from its file) */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"ACQUIRE", ACQUIRE}, {"ACQ_DDC", ACQ_DDC},
@ -253,6 +258,9 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"ACQ_EXCHANGE", ACQ_EXCHANGE},
{"ACQ_RETURNQRY", ACQ_RETURNQRY},
{"ACQ_RETURNSTAT", ACQ_RETURNSTAT},
{"ACQ_DISPUTE_OPEN", ACQ_DISPUTE_OPEN},
{"ACQ_DISPUTE_ADVANCE", ACQ_DISPUTE_ADVANCE},
{"ACQ_PRESENT", ACQ_PRESENT},
{NULL, NULL}
};

View file

@ -0,0 +1,25 @@
/*
* ACQ_DISPUTE_ADVANCE.h - ac ACQ_DISPUTE_ADVANCE (copybook / record header).
* : FIRST_CBREPRESENTPRE_ARBARBITRATIONWON/LOST,
* ACCEPTACCEPTED. (FAIL).
* Request fields: T_ID1(dispute_id), T_STR1(action), [T_STR2( stage ), T_PURCHASE_ID]
* Response fields: T_STATUS( stage status), T_ID1, T_ARG1(resolve flag), T_RC
*/
#ifndef ACQ_DISPUTE_ADVANCE_H
#define ACQ_DISPUTE_ADVANCE_H
#include "ac_svc.h"
/* 분쟁 전이 결과 (내부 판정 결과 표현용) */
typedef struct {
long dispute_id;
char from_stage[16];
char action[16];
char to_stage[16];
char to_status[16];
long resolved; /* 1 = 종결 전이(resolved_at 세팅) */
} ac_dispute_move_t;
void ACQ_DISPUTE_ADVANCE(TPSVCINFO *p);
#endif /* ACQ_DISPUTE_ADVANCE_H */

View file

@ -0,0 +1,182 @@
/*
* ACQ_DISPUTE_ADVANCE.pgc - ac 모듈 서비스 ACQ_DISPUTE_ADVANCE (one service = one file).
*
* 분쟁 라이프사이클 상태기계. 카드 분쟁은 정해진 순서로만 진행하며, 이 서비스는
* 현재 stage/status 를 읽어 요청 action 에 대한 "적법한 전이만" 허용하고 나머지는 거부한다.
*
* stage/action 전이표 (status='OPEN' 인 열린 분쟁에서만):
* FIRST_CB --REPRESENT--> REPRESENT (매입사 재청구 방어)
* REPRESENT --ESCALATE---> PRE_ARB (사전 중재 상신)
* PRE_ARB --ESCALATE---> ARBITRATION (중재 상신)
* ARBITRATION --WIN--------> status=WON (종결, resolved_at)
* ARBITRATION --LOSE-------> status=LOST (종결, resolved_at)
* <any open> --ACCEPT-----> status=ACCEPTED (책임 인수, 종결)
* 그 외 (stage,action) 조합 = 부적절한 전이 → T_RC 세팅 후 FAIL.
* 이미 WON/LOST/ACCEPTED 로 종결된 분쟁은 어떤 전이도 거부.
*
* 입출력 계약: 입력 T_ID1(dispute_id) + T_STR1(action)[+ T_STR2(현재 stage 힌트)],
* 출력 T_STATUS(새 stage 또는 종결 status).
* Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr.
*/
#include "ACQ_DISPUTE_ADVANCE.h"
void ACQ_DISPUTE_ADVANCE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *SVC = "ACQ_DISPUTE_ADVANCE";
char action[24], bizdate[16], newstage[16], newstatus[16], memo[256], logmsg[256];
long resolve = 0, valid = 0, i = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_did, h_pid, h_amt, h_logid, h_open;
char h_stage[16], h_status[16], h_merch[64], h_reason[64];
char h_new_stage[16], h_new_status[16], h_bizdate[16], h_memo[256], h_logmsg[256];
EXEC SQL END DECLARE SECTION;
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
h_did = h_pid = h_amt = h_logid = h_open = 0;
action[0] = bizdate[0] = newstage[0] = newstatus[0] = memo[0] = logmsg[0] = 0;
h_stage[0] = h_status[0] = h_reason[0] = h_bizdate[0] = 0;
strcpy(h_merch, "UNKNOWN");
userlog("%s 진입: 분쟁 단계 전이 요청", SVC);
/* [1] 입력 검증부 -------------------------------------------------- */
h_did = getl(b, T_ID1);
if (h_did <= 0) {
Bchg(b, T_MSG, 0, "분쟁 ID(T_ID1) 오류", 0L); setl(b, T_RC, 7301); FAIL(b);
}
gets_(b, T_STR1, action, sizeof(action));
if (action[0] == 0) {
Bchg(b, T_MSG, 0, "action(T_STR1) 필수", 0L); setl(b, T_RC, 7302); FAIL(b);
}
for (i = 0; action[i]; i++) /* action 대문자 정규화 */
if (action[i] >= 'a' && action[i] <= 'z') action[i] -= 32;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (h_bizdate[0] == 0) {
EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE';
if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19");
}
/* [2] 마스터 조회부: 현재 분쟁 상태 (전이의 기준) ------------------- */
EXEC SQL SELECT purchase_id, merchant_id, amount, reason_code, stage, status
INTO :h_pid, :h_merch, :h_amt, :h_reason, :h_stage, :h_status
FROM ac_dispute WHERE dispute_id = :h_did;
if (sqlca.sqlcode < 0) {
userlog("%s: 분쟁 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
setl(b, T_RC, 90); FAIL(b);
}
if (sqlca.sqlcode == 100) {
/* XA 격리: 형제 브랜치가 방금 개시한 미커밋 분쟁이면 전문에 실린 현재 stage 를 쓴다.
* 전문 힌트(T_STR2)도 없으면 진짜 미존재이므로 업무검증 실패로 거부한다. */
gets_(b, T_STR2, h_stage, sizeof(h_stage));
gets_(b, T_STATUS, h_status, sizeof(h_status));
if (h_status[0] == 0) strcpy(h_status, "OPEN");
h_pid = getl(b, T_PURCHASE_ID);
h_amt = getl(b, T_AMOUNT);
if (h_stage[0] == 0) {
userlog("%s: 분쟁 미존재 did=%ld (전문 힌트 없음)", SVC, h_did);
Bchg(b, T_MSG, 0, "분쟁 미존재", 0L); setl(b, T_RC, 7303); FAIL(b);
}
userlog("%s: 분쟁 미커밋(체인) did=%ld — 전문 stage=%s 사용", SVC, h_did, h_stage);
}
userlog("%s 컨텍스트: did=%ld pid=%ld merch=%s stage=%s status=%s action=%s",
SVC, h_did, h_pid, h_merch, h_stage, h_status, action);
/* [3] 종결 상태 확인부: 이미 resolved 면 어떤 전이도 불가 ----------- */
if (strcmp(h_status, "WON") == 0 || strcmp(h_status, "LOST") == 0 ||
strcmp(h_status, "ACCEPTED") == 0) {
snprintf(logmsg, sizeof(logmsg), "이미 종결된 분쟁 status=%s 전이 거부", h_status);
userlog("%s %s did=%ld", SVC, logmsg, h_did);
Bchg(b, T_MSG, 0, logmsg, 0L);
Bchg(b, T_STATUS, 0, h_status, 0L);
setl(b, T_RC, 7310); FAIL(b);
}
/* [4] 상태기계 전이 판정부 ----------------------------------------- */
strcpy(newstage, h_stage); strcpy(newstatus, h_status);
resolve = 0; valid = 0;
if (strcmp(action, "ACCEPT") == 0) {
/* 어느 열린 단계에서든 가맹점/매입사가 책임을 인수 → ACCEPTED(종결) */
strcpy(newstatus, "ACCEPTED"); resolve = 1; valid = 1;
} else if (strcmp(h_stage, "FIRST_CB") == 0 && strcmp(action, "REPRESENT") == 0) {
strcpy(newstage, "REPRESENT"); valid = 1;
} else if (strcmp(h_stage, "REPRESENT") == 0 && strcmp(action, "ESCALATE") == 0) {
strcpy(newstage, "PRE_ARB"); valid = 1;
} else if (strcmp(h_stage, "PRE_ARB") == 0 && strcmp(action, "ESCALATE") == 0) {
strcpy(newstage, "ARBITRATION"); valid = 1;
} else if (strcmp(h_stage, "ARBITRATION") == 0 &&
(strcmp(action, "WIN") == 0 || strcmp(action, "WON") == 0)) {
strcpy(newstatus, "WON"); resolve = 1; valid = 1;
} else if (strcmp(h_stage, "ARBITRATION") == 0 &&
(strcmp(action, "LOSE") == 0 || strcmp(action, "LOSS") == 0 ||
strcmp(action, "LOST") == 0)) {
strcpy(newstatus, "LOST"); resolve = 1; valid = 1;
}
if (!valid) {
snprintf(logmsg, sizeof(logmsg), "부적절한 전이 거부: stage=%s action=%s", h_stage, action);
userlog("%s %s did=%ld", SVC, logmsg, h_did);
Bchg(b, T_MSG, 0, logmsg, 0L);
Bchg(b, T_STATUS, 0, h_stage, 0L); /* 현재 stage 유지 통지 */
setl(b, T_RC, 7320); FAIL(b);
}
userlog("%s 전이확정: %s/%s --%s--> stage=%s status=%s resolve=%ld",
SVC, h_stage, h_status, action, newstage, newstatus, resolve);
/* [5] 본처리부: ac_dispute UPDATE (종결 전이면 resolved_at 세팅) ----- */
strncpy(h_new_stage, newstage, sizeof(h_new_stage)-1); h_new_stage[sizeof(h_new_stage)-1] = 0;
strncpy(h_new_status, newstatus, sizeof(h_new_status)-1); h_new_status[sizeof(h_new_status)-1] = 0;
snprintf(memo, sizeof(memo), "전이 %s/%s -%s-> %s/%s @%s",
h_stage, h_status, action, h_new_stage, h_new_status, h_bizdate);
strncpy(h_memo, memo, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1] = 0;
if (resolve) {
EXEC SQL UPDATE ac_dispute
SET stage = :h_new_stage, status = :h_new_status, resolved_at = now(), memo = :h_memo
WHERE dispute_id = :h_did;
} else {
EXEC SQL UPDATE ac_dispute
SET stage = :h_new_stage, status = :h_new_status, memo = :h_memo
WHERE dispute_id = :h_did;
}
if (sqlca.sqlcode < 0) {
userlog("%s: 분쟁 갱신 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
setl(b, T_RC, 90); FAIL(b);
}
if (sqlca.sqlcode == 100) {
/* XA 격리: 대상 행이 이 브랜치에 안 보임(체인 미커밋). 전문 기반으로 진행하고 FAIL 안 함 */
userlog("%s: UPDATE 대상 미가시(체인 미커밋) did=%ld — 전문값 응답", SVC, h_did);
}
/* [6] 이력/감사 적재부 (cm_log) ------------------------------------- */
snprintf(logmsg, sizeof(logmsg), "분쟁전이 did=%ld %s->%s/%s action=%s resolve=%ld",
h_did, h_stage, h_new_stage, h_new_status, action, resolve);
strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0;
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
VALUES (:h_logid, 'ACQ_DISPUTE_ADVANCE', 'I', :h_logmsg, :h_bizdate);
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
/* [7] 집계/요약부: 가맹점 진행중 분쟁 수 재집계 --------------------- */
EXEC SQL SELECT count(*) INTO :h_open FROM ac_dispute
WHERE merchant_id = :h_merch AND status = 'OPEN';
if (sqlca.sqlcode < 0) { h_open = 0; } /* 집계 실패는 응답을 막지 않음 */
/* [8] 응답부 (계약 보존: T_STATUS = 새 stage 또는 종결 status) ------- */
if (resolve) Bchg(b, T_STATUS, 0, h_new_status, 0L);
else Bchg(b, T_STATUS, 0, h_new_stage, 0L);
setl(b, T_ID1, h_did);
setl(b, T_PURCHASE_ID, h_pid);
setl(b, T_AMOUNT, h_amt);
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
Bchg(b, T_STR2, 0, h_new_stage, 0L); /* 새 stage 힌트(체인 전달용) */
Bchg(b, T_STR1, 0, action, 0L);
setl(b, T_ARG1, resolve);
setl(b, T_COUNT, h_open);
Bchg(b, T_MSG, 0, h_memo, 0L);
setl(b, T_RC, 0);
userlog("%s 완료: did=%ld -> stage=%s status=%s (resolve=%ld)",
SVC, h_did, h_new_stage, h_new_status, resolve);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,24 @@
/*
* ACQ_DISPUTE_OPEN.h - ac ACQ_DISPUTE_OPEN (copybook / record header).
* / : 1 ac_dispute FIRST_CB/OPEN .
* Request fields: T_PURCHASE_ID, T_STR1(reason_code), [T_AMOUNT, T_MERCHANT, T_BIZDATE]
* Response fields: T_ID1(dispute_id), T_STATUS='FIRST_CB', T_AMOUNT, T_COUNT, T_ARG1(tier)
*/
#ifndef ACQ_DISPUTE_OPEN_H
#define ACQ_DISPUTE_OPEN_H
#include "ac_svc.h"
typedef struct {
long dispute_id;
long purchase_id;
char merchant_id[64];
char reason_code[64];
long amount;
char stage[16]; /* FIRST_CB */
char status[16]; /* OPEN */
} ac_dispute_rec_t;
void ACQ_DISPUTE_OPEN(TPSVCINFO *p);
#endif /* ACQ_DISPUTE_OPEN_H */

View file

@ -0,0 +1,136 @@
/*
* ACQ_DISPUTE_OPEN.pgc - ac 모듈 서비스 ACQ_DISPUTE_OPEN (one service = one file).
* 채그백/분쟁 개시: 매입 원장을 확인하고 ac_dispute 에 FIRST_CB/OPEN 분쟁행을 만든다.
* 분쟁금액은 매입금액, reason_code 는 카드망 분쟁코드(10.4 사기 / 13.1 상품미수취 /
* 12.6 중복 ...)를 그대로 적재하고 사기·상품·중복 등급(tier)으로 분류한다.
* 입출력 계약: 입력 T_PURCHASE_ID + T_STR1(reason_code)[+ T_AMOUNT/T_MERCHANT/T_BIZDATE],
* 출력 T_ID1(dispute_id) / T_STATUS='FIRST_CB'.
* Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr.
*/
#include "ACQ_DISPUTE_OPEN.h"
void ACQ_DISPUTE_OPEN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *SVC = "ACQ_DISPUTE_OPEN";
char bizdate[16], reason[64], memo[256], logmsg[256];
long tier = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_amt, h_did, h_logid, h_dup, h_cnt, h_merchcb;
char h_bizdate[16], h_reason[64], h_merch[64], h_pstat[16];
char h_memo[256], h_logmsg[256];
EXEC SQL END DECLARE SECTION;
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
h_pid = h_amt = h_did = h_logid = h_dup = h_cnt = h_merchcb = 0;
bizdate[0] = reason[0] = memo[0] = logmsg[0] = 0;
h_bizdate[0] = h_reason[0] = 0;
strcpy(h_merch, "UNKNOWN"); strcpy(h_pstat, "S");
userlog("%s 진입: 분쟁/채그백 개시 요청", SVC);
/* [1] 입력 검증부 -------------------------------------------------- */
h_pid = getl(b, T_PURCHASE_ID);
if (h_pid <= 0) {
Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 7201); FAIL(b);
}
gets_(b, T_STR1, reason, sizeof(reason));
if (reason[0] == 0) strcpy(reason, "13.1"); /* 기본: 13.1 상품미수취 */
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (h_bizdate[0] == 0) {
EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE';
if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19");
}
/* [2] 마스터/상태 조회부 (XA 격리: 미커밋이면 전문값 사용, 100 을 FAIL 로 다루지 않음) */
EXEC SQL SELECT amount, merchant_id, status INTO :h_amt, :h_merch, :h_pstat
FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
userlog("%s: 매입원장 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
setl(b, T_RC, 90); FAIL(b);
}
if (sqlca.sqlcode == 100) {
/* 체인 내 신규 매입(미커밋): 요청 전문에 실린 값으로 진행 */
h_amt = getl(b, T_AMOUNT);
gets_(b, T_MERCHANT, h_merch, sizeof(h_merch));
if (h_merch[0] == 0) strcpy(h_merch, "UNKNOWN");
strcpy(h_pstat, "S");
userlog("%s: 매입 미커밋(체인) pid=%ld — 전문값 사용", SVC, h_pid);
}
if (h_amt <= 0) h_amt = getl(b, T_AMOUNT);
if (h_amt <= 0) {
Bchg(b, T_MSG, 0, "분쟁 금액 산정 불가", 0L); setl(b, T_RC, 7202); FAIL(b);
}
userlog("%s 컨텍스트: pid=%ld merch=%s 상태=%s 금액=%ld reason=%s",
SVC, h_pid, h_merch, h_pstat, h_amt, h_reason);
/* [3] 업무규칙 분기부: 중복 분쟁 방지 + reason 등급 분류 -------------- */
EXEC SQL SELECT count(*) INTO :h_dup FROM ac_dispute
WHERE purchase_id = :h_pid AND status = 'OPEN';
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
if (h_dup > 0) {
userlog("%s 거절: 이미 진행중인 분쟁 존재 pid=%ld", SVC, h_pid);
Bchg(b, T_MSG, 0, "이미 진행중인 분쟁 존재", 0L); setl(b, T_RC, 7203); FAIL(b);
}
/* 무효/취소된 매입은 분쟁 개시 자체가 성립하지 않는다 */
if (strcmp(h_pstat, "V") == 0) {
Bchg(b, T_MSG, 0, "무효 매입은 분쟁 불가", 0L); setl(b, T_RC, 7204); FAIL(b);
}
/* reason 등급: 사기(10.x)=3 최우선, 상품(13.x)=2, 중복(12.x)=1, 그 외=0 */
if (strncmp(h_reason, "10.", 3) == 0) tier = 3;
else if (strncmp(h_reason, "13.", 3) == 0) tier = 2;
else if (strncmp(h_reason, "12.", 3) == 0) tier = 1;
else tier = 0;
/* [4] 본처리부: dispute INSERT (FIRST_CB / OPEN) --------------------- */
EXEC SQL SELECT nextval('ac_dispute_seq') INTO :h_did;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
snprintf(memo, sizeof(memo), "채그백 개시 pid=%ld reason=%s tier=%ld bizdate=%s",
h_pid, h_reason, tier, h_bizdate);
strncpy(h_memo, memo, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1] = 0;
EXEC SQL INSERT INTO ac_dispute
(dispute_id, purchase_id, merchant_id, reason_code, amount, stage, status, memo)
VALUES (:h_did, :h_pid, :h_merch, :h_reason, :h_amt, 'FIRST_CB', 'OPEN', :h_memo);
if (sqlca.sqlcode < 0) {
userlog("%s: 분쟁행 적재 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
setl(b, T_RC, 90); FAIL(b);
}
/* [5] 이력/감사 적재부 (cm_log) ------------------------------------- */
snprintf(logmsg, sizeof(logmsg), "분쟁개시 did=%ld pid=%ld reason=%s amt=%ld tier=%ld",
h_did, h_pid, h_reason, h_amt, tier);
strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0;
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
VALUES (:h_logid, 'ACQ_DISPUTE_OPEN', 'I', :h_logmsg, :h_bizdate);
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
/* [6] 집계/요약 갱신부: 가맹점 진행중 분쟁 수 + LOST 확정 채그백 누계 ---- */
EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_dispute
WHERE merchant_id = :h_merch AND status = 'OPEN';
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_merchcb FROM ac_dispute
WHERE merchant_id = :h_merch AND status = 'LOST';
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
userlog("%s 집계: merch=%s 진행중=%ld LOST누계=%ld", SVC, h_merch, h_cnt, h_merchcb);
/* [7] 응답부 (계약 보존: T_ID1=dispute_id / T_STATUS='FIRST_CB') ------ */
setl(b, T_ID1, h_did);
Bchg(b, T_STATUS, 0, "FIRST_CB", 0L);
setl(b, T_PURCHASE_ID, h_pid);
setl(b, T_AMOUNT, h_amt);
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
Bchg(b, T_STR1, 0, h_reason, 0L);
Bchg(b, T_BIZDATE, 0, h_bizdate, 0L);
setl(b, T_ARG1, tier);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_merchcb);
Bchg(b, T_MSG, 0, "분쟁 개시(FIRST_CB/OPEN)", 0L);
setl(b, T_RC, 0);
userlog("%s 완료: did=%ld pid=%ld amt=%ld tier=%ld", SVC, h_did, h_pid, h_amt, tier);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,26 @@
/*
* ACQ_PRESENT.h - ac ACQ_PRESENT (copybook / record header).
* () : · .
* clearing_amt = amount interchange ( ).
* Request fields: T_PURCHASE_ID, [T_STR1(scheme), T_AMOUNT, T_AMT1(interchange), T_BIZDATE]
* Response fields: T_ID1(presentment_id), T_AMT1(clearing_amt), T_STATUS='CLEARED', T_FEE(ic)
*/
#ifndef ACQ_PRESENT_H
#define ACQ_PRESENT_H
#include "ac_svc.h"
typedef struct {
long presentment_id;
long purchase_id;
char scheme[16];
char msg_class[24]; /* FIRST_PRESENT / REPRESENT */
long clearing_amt;
char present_date[16];
char cycle[12]; /* T+1 */
char status[16]; /* CLEARED */
} cl_presentment_rec_t;
void ACQ_PRESENT(TPSVCINFO *p);
#endif /* ACQ_PRESENT_H */

View file

@ -0,0 +1,140 @@
/*
* ACQ_PRESENT.pgc - ac 모듈 서비스 ACQ_PRESENT (one service = one file).
* 프레젠트먼트(클리어링) 생성. 승인·정산과 분리된 데이터 교환 단계로, 발급사↔매입사
* 채무를 확정한다. 수수료 3층 분해(st_fee_component)의 interchange 는 발급사 귀속분이므로
* 클리어링 대상 금액은 clearing_amt = amount interchange 이다.
* 같은 매입에 FIRST_PRESENT 가 이미 있으면 msg_class 를 REPRESENT 로 승격한다.
* 입출력 계약: 입력 T_PURCHASE_ID [+ T_STR1(scheme)/T_AMOUNT/T_AMT1(interchange)/T_BIZDATE],
* 출력 T_ID1(presentment_id) / T_AMT1(clearing_amt) / T_STATUS='CLEARED'.
* Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr.
*/
#include "ACQ_PRESENT.h"
void ACQ_PRESENT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *SVC = "ACQ_PRESENT";
char bizdate[16], scheme[16], msgclass[24], cycle[12], logmsg[256];
long clearing = 0, has_fc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_amt, h_ic, h_clr, h_prid, h_logid, h_cnt, h_prev;
char h_bizdate[16], h_scheme[16], h_msgclass[24], h_cycle[12], h_merch[64], h_logmsg[256];
EXEC SQL END DECLARE SECTION;
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
h_pid = h_amt = h_ic = h_clr = h_prid = h_logid = h_cnt = h_prev = 0;
bizdate[0] = logmsg[0] = 0;
strcpy(scheme, "BC"); strcpy(cycle, "T+1"); strcpy(msgclass, "FIRST_PRESENT");
h_bizdate[0] = 0; strcpy(h_scheme, "BC"); strcpy(h_merch, "UNKNOWN");
userlog("%s 진입: 프레젠트먼트(클리어링) 생성 요청", SVC);
/* [1] 입력 검증부 -------------------------------------------------- */
h_pid = getl(b, T_PURCHASE_ID);
if (h_pid <= 0) {
Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 7401); FAIL(b);
}
gets_(b, T_STR1, scheme, sizeof(scheme));
if (scheme[0] == 0) strcpy(scheme, "BC");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
/* [2] 수수료 분해 조회부 (XA 격리: 없으면 전문 amount/interchange, 100 은 정상흐름) */
EXEC SQL SELECT amount, interchange, scheme INTO :h_amt, :h_ic, :h_scheme
FROM st_fee_component WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
userlog("%s: 수수료 분해 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
setl(b, T_RC, 90); FAIL(b);
}
if (sqlca.sqlcode == 100) {
/* 분해 미산출/미커밋: 전문값으로 클리어링. interchange 미제공이면 0 */
has_fc = 0;
h_amt = getl(b, T_AMOUNT);
h_ic = getl(b, T_AMT1);
strncpy(h_scheme, scheme, sizeof(h_scheme)-1); h_scheme[sizeof(h_scheme)-1] = 0;
userlog("%s: st_fee_component 미존재 pid=%ld — 전문 amount=%ld ic=%ld 사용",
SVC, h_pid, h_amt, h_ic);
} else {
has_fc = 1;
}
if (h_amt <= 0) h_amt = getl(b, T_AMOUNT);
if (h_amt <= 0) {
Bchg(b, T_MSG, 0, "클리어링 금액 산정 불가", 0L); setl(b, T_RC, 7402); FAIL(b);
}
if (h_ic < 0) h_ic = 0;
if (h_ic > h_amt) h_ic = h_amt; /* interchange 는 매출을 넘지 못함 */
if (h_scheme[0] == 0) strcpy(h_scheme, "BC");
/* 매입 원장에서 영업일/가맹점 보강 (미커밋이면 전문/파라미터 폴백) */
EXEC SQL SELECT merchant_id, biz_date INTO :h_merch, :h_bizdate
FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
if (sqlca.sqlcode == 100) {
gets_(b, T_MERCHANT, h_merch, sizeof(h_merch));
if (h_merch[0] == 0) strcpy(h_merch, "UNKNOWN");
userlog("%s: 매입 미커밋(체인) pid=%ld — 전문값 사용", SVC, h_pid);
}
if (h_bizdate[0] == 0) {
EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE';
if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19");
}
userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld ic=%ld scheme=%s fc=%ld",
SVC, h_pid, h_merch, h_amt, h_ic, h_scheme, has_fc);
/* [3] 업무규칙 분기부: 기존 프레젠트먼트 존재 여부로 msg_class 결정 ---- */
EXEC SQL SELECT count(*) INTO :h_prev FROM cl_presentment
WHERE purchase_id = :h_pid AND msg_class = 'FIRST_PRESENT';
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
if (h_prev > 0) strcpy(msgclass, "REPRESENT"); /* 1차가 있으면 재프레젠트 */
else strcpy(msgclass, "FIRST_PRESENT");
strncpy(h_msgclass, msgclass, sizeof(h_msgclass)-1); h_msgclass[sizeof(h_msgclass)-1] = 0;
strncpy(h_cycle, cycle, sizeof(h_cycle)-1); h_cycle[sizeof(h_cycle)-1] = 0;
/* 클리어링 금액 = 매출 발급사 귀속 interchange */
clearing = h_amt - h_ic;
if (clearing < 0) clearing = 0;
h_clr = clearing;
/* [4] 본처리부: cl_presentment INSERT (CLEARED) --------------------- */
EXEC SQL SELECT nextval('cl_present_seq') INTO :h_prid;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
EXEC SQL INSERT INTO cl_presentment
(presentment_id, purchase_id, scheme, msg_class, clearing_amt, present_date, cycle, status)
VALUES (:h_prid, :h_pid, :h_scheme, :h_msgclass, :h_clr, :h_bizdate, :h_cycle, 'CLEARED');
if (sqlca.sqlcode < 0) {
userlog("%s: 프레젠트먼트 적재 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
setl(b, T_RC, 90); FAIL(b);
}
/* [5] 이력/감사 적재부 (cm_log) ------------------------------------- */
snprintf(logmsg, sizeof(logmsg), "프레젠트먼트 prid=%ld pid=%ld class=%s clr=%ld ic=%ld",
h_prid, h_pid, h_msgclass, clearing, h_ic);
strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0;
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
VALUES (:h_logid, 'ACQ_PRESENT', 'I', :h_logmsg, :h_bizdate);
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
/* [6] 집계/요약부: 해당 매입 프레젠트먼트 총 건수 ------------------- */
EXEC SQL SELECT count(*) INTO :h_cnt FROM cl_presentment WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_cnt = 0; }
/* [7] 응답부 (계약 보존: T_ID1=presentment_id / T_AMT1=clearing_amt) - */
setl(b, T_ID1, h_prid);
setl(b, T_AMT1, clearing);
setl(b, T_PURCHASE_ID, h_pid);
setl(b, T_AMOUNT, h_amt);
setl(b, T_FEE, h_ic);
Bchg(b, T_STATUS, 0, "CLEARED", 0L);
Bchg(b, T_STR1, 0, h_scheme, 0L);
Bchg(b, T_STR2, 0, h_msgclass, 0L);
Bchg(b, T_BIZDATE, 0, h_bizdate, 0L);
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
setl(b, T_COUNT, h_cnt);
Bchg(b, T_MSG, 0, "프레젠트먼트 생성(CLEARED)", 0L);
setl(b, T_RC, 0);
userlog("%s 완료: prid=%ld pid=%ld clr=%ld class=%s", SVC, h_prid, h_pid, clearing, h_msgclass);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -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}
};

View 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 */

View 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: */

View 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 */

View 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: */

View 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 */

View 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: */

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":4626,"u":"개"},{"k":"총 코드 라인","v":330642,"u":"LOC"},{"k":"온라인 서비스","v":1463,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23742,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":46904,"u":"곳"},{"k":"DB 테이블","v":75,"u":"개"},{"k":"카피북 헤더(.h)","v":2207,"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":23742,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":46904,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6892,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2247,"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":2207,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2247,"goto":64,"userlog":17122,"sqlca_checks":18719,"embedded_sql_density":9.8}};
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}};