Wave J: 카드보안·자금·규제 고도화 (패리티 70%→80%)

패리티 분석의 최우선 갭(카드보안·자금연동·운영·규제)을 데이터모델+시드+
런타임서비스+화면으로 체계적 보강. 전부 실측·런타임 검증.

■ J-1 카드보안 (승인 60→82)
- card_vault(6,680): PCI 토큰볼트 — PAN 원문 미저장, 마스킹+SHA256 해시.
- au_3ds(1,235): 3DS/SCA — ECI·CAVV·책임전환(ISSUER 1,143).
- au_verify(8,191): AVS/CVV + 리스크판정(APPROVE/REVIEW/DECLINE).
- 서비스 TOKENIZE/AUTH_3DS/AUTH_VERIFY (au).

■ J-2 자금이동 (정산 75→85)
- st_funding_instr(355k): 펌뱅킹 펀딩지시/반환(CONFIRMED/RETURNED).
- ac_dcc(426): DCC 다통화 승인시 환율고정(USD/JPY/EUR/CNY + 마크업 3%).
- 서비스 ST_FUNDINSTR/AC_DCC.

■ J-3 수수료·분쟁 (수수료 80→88, 분쟁 70→82)
- st_cb_fee(175): 채그백 건당수수료(단계별 차등).
- ac_retrieval(127) + 분쟁 SLA타이머·fraud_flag. 서비스 ACQ_RETRIEVAL.

■ J-4 리포팅·규제 (리포팅 70→85)
- rp_merchant_stmt(12.8k): 가맹점 월명세서.
- rp_regulatory(134): 여신전문금융업법 정산주기 준수·거래량 보고.

■ J-5 화면·검증
- 포털 "카드보안·규제"(SY0004, #sec): 토큰볼트·3DS·AVS/CVV·DCC·펀딩·규제.
- 통합빌드 error 0, 5,907 서비스 AVAIL, 6본 신규서비스 실호출 검증(미결2PC 0).
- PARITY_ANALYSIS 재채점: 가중 ≈80/100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyeongwoo 2026-07-23 13:00:07 +09:00
parent 782b802156
commit d292772b1f
21 changed files with 1337 additions and 7 deletions

View file

@ -155,6 +155,8 @@
#include "ST_FEEDECOMP.h"
#include "ST_NETSETTLE.h"
#include "ST_FUNDING.h"
/* Wave J — 자금이동 실연동(은행 rails 펀딩 지시) */
#include "ST_FUNDINSTR.h"
/* ----- advertise table: {service name, function} ---------------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
@ -226,7 +228,7 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"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},
{"ST_FUNDING", ST_FUNDING}, {"ST_FUNDINSTR", ST_FUNDINSTR},
{NULL, NULL}
};

View file

@ -0,0 +1,26 @@
/*
* ST_FUNDINSTR.h - st ST_FUNDINSTR (copybook / record header).
* /: st_merchant_funding.deposit rails(FIRMBANK)
* st_funding_instr . deposit<=0 RETURNED().
* Request fields: T_MERCHANT, T_BIZDATE
* Response fields: T_AMT1(amount), T_STATUS(status), T_ID1(instr_id)
*/
#ifndef ST_FUNDINSTR_H
#define ST_FUNDINSTR_H
#include "st_svc.h"
typedef struct {
long instr_id;
char merchant_id[64];
char biz_date[16];
char bank_code[16];
char account_no[24];
long amount; /* 지급액(펀딩 deposit) */
char rail[16]; /* FIRMBANK/ACH/RTGS */
char status[16]; /* SENT/CONFIRMED/RETURNED */
} st_fundinstr_rec_t;
void ST_FUNDINSTR(TPSVCINFO *p);
#endif /* ST_FUNDINSTR_H */

View file

@ -0,0 +1,151 @@
/*
* ST_FUNDINSTR.pgc - st (정산/수수료) 모듈 서비스 ST_FUNDINSTR (one service = one file).
* 펀딩 지시 생성/확인: 가맹점 순액정산 결과(st_merchant_funding.deposit)를 은행
* rails(FIRMBANK) 지급지시로 전환해 st_funding_instr 에 적재한다. 이미 당일
* 지시가 있으면 재생성 없이 "확인"으로 그 지시를 반환한다(멱등). deposit<=0 이면
* 지급 불가로 RETURNED(반환) 지시를 남긴다.
* 처리단계: 입력검증 -> 펀딩(deposit) 조회 -> 기지시 확인 -> 업무규칙(반환/지급) ->
* 본처리(st_funding_instr INSERT) -> 감사이력 -> 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). st_svr 가 tpsvrinit 에서 광고.
* XA 격리: 펀딩/기지시는 본 브랜치에서 조회 가능한 커밋행 기준으로 판정한다.
*/
#include "ST_FUNDINSTR.h"
/* ST_FUNDINSTR - 펀딩 지시 생성/확인(은행 rails 지급지시). */
void ST_FUNDINSTR(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
char merch[64], bizdate[16], msgbuf[220];
unsigned long hv;
int i, banksel;
static const char *BANKS[5] = { "KB", "SHINHAN", "HANA", "NH", "WOORI" };
EXEC SQL BEGIN DECLARE SECTION;
long h_deposit, h_amount, h_instr, h_exist, h_chk;
char h_merch[64], h_bizdate[16], h_bank[16], h_acct[24], h_rail[16];
char h_status[16], h_rcode[32], h_estatus[16], h_svc[24], h_msg[220];
EXEC SQL END DECLARE SECTION;
strcpy(h_svc, "ST_FUNDINSTR");
h_deposit = h_amount = h_instr = h_exist = h_chk = 0;
strcpy(h_rail, "FIRMBANK"); strcpy(h_status, "SENT");
strcpy(h_bank, "KB"); h_acct[0] = 0; h_rcode[0] = 0; h_estatus[0] = 0;
/* ===== 1) 입력 검증부 ===== */
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
userlog("[자금연동] 펀딩 지시 진입 merch=%s biz=%s", merch, bizdate);
if (merch[0] == 0) {
setl(b, T_RC, 8601); Bchg(b, T_MSG, 0, "가맹점 ID(merchant_id) 필수", 0L);
userlog("[ST_FUNDINSTR] 가맹점 ID 누락"); FAIL(b);
}
if (strlen(bizdate) < 8) {
setl(b, T_RC, 8602); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L);
userlog("[ST_FUNDINSTR] 영업일 검증 실패 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;
/* 은행/계좌 결정: 가맹점 ID 해시로 결정(마스터 없는 데모 지급계좌). */
hv = 5381UL;
for (i = 0; h_merch[i] != 0; i++) hv = ((hv << 5) + hv) + (unsigned char)h_merch[i];
banksel = (int)(hv % 5);
strncpy(h_bank, BANKS[banksel], sizeof(h_bank)-1); h_bank[sizeof(h_bank)-1] = 0;
snprintf(h_acct, sizeof(h_acct), "%012lu", (hv >> 3) % 1000000000000UL);
/* ===== 2) 펀딩(deposit) 조회부 ===== */
EXEC SQL SELECT deposit INTO :h_deposit FROM st_merchant_funding
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
if (sqlca.sqlcode == 100) {
/* 당일 펀딩 산출이 없으면 지시 대상 아님(무처리 종료 — ST_FUNDING NODATA 규율). */
Bchg(b, T_STATUS, 0, "NOFUND", 0L);
setl(b, T_AMT1, 0); setl(b, T_ID1, 0);
Bchg(b, T_SVCNAME, 0, "ST_FUNDINSTR", 0L); setl(b, T_RC, 0);
userlog("[ST_FUNDINSTR] 펀딩 미산출 merch=%s biz=%s — 지시 대상 없음", h_merch, h_bizdate);
OK(b);
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8610); Bchg(b, T_MSG, 0, "펀딩 조회 오류", 0L);
userlog("[ST_FUNDINSTR] 펀딩 조회 실패 [%d]", sqlca.sqlcode); FAIL(b);
}
h_amount = h_deposit;
/* ===== 3) 기지시 확인부 (멱등: 이미 있으면 그 지시를 반환) ===== */
EXEC SQL SELECT instr_id, amount, status INTO :h_exist, :h_amount, :h_estatus
FROM st_funding_instr WHERE merchant_id = :h_merch AND biz_date = :h_bizdate
ORDER BY instr_id LIMIT 1;
if (sqlca.sqlcode == 0) {
setl(b, T_AMT1, h_amount);
Bchg(b, T_STATUS, 0, h_estatus, 0L);
setl(b, T_ID1, h_exist);
Bchg(b, T_SVCNAME, 0, "ST_FUNDINSTR", 0L); setl(b, T_RC, 0);
userlog("[ST_FUNDINSTR] 기지시 확인 instr=%ld amount=%ld status=%s", h_exist, h_amount, h_estatus);
OK(b);
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8611); Bchg(b, T_MSG, 0, "기지시 조회 오류", 0L); FAIL(b);
}
h_amount = h_deposit;
/* ===== 4) 업무규칙 분기부 (지급/반환) ===== */
if (h_deposit <= 0) {
strcpy(h_status, "RETURNED");
strcpy(h_rcode, "R02-지급액없음");
userlog("[ST_FUNDINSTR] 입금액 비양수 deposit=%ld — 반환 지시 생성", h_deposit);
} else {
strcpy(h_status, "SENT"); h_rcode[0] = 0;
}
/* ===== 5) 본처리부 (st_funding_instr INSERT) ===== */
EXEC SQL SELECT nextval('st_fundinstr_seq') INTO :h_instr;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8620); Bchg(b, T_MSG, 0, "지시 시퀀스 실패", 0L); FAIL(b); }
if (h_rcode[0] == 0) {
EXEC SQL INSERT INTO st_funding_instr
(instr_id, merchant_id, biz_date, bank_code, account_no, amount, rail, status)
VALUES (:h_instr, :h_merch, :h_bizdate, :h_bank, :h_acct, :h_amount, :h_rail, :h_status);
} else {
EXEC SQL INSERT INTO st_funding_instr
(instr_id, merchant_id, biz_date, bank_code, account_no, amount, rail, status, return_code)
VALUES (:h_instr, :h_merch, :h_bizdate, :h_bank, :h_acct, :h_amount, :h_rail, :h_status, :h_rcode);
}
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8621); Bchg(b, T_MSG, 0, "펀딩 지시 적재 실패", 0L);
userlog("[ST_FUNDINSTR] st_funding_instr INSERT 실패 [%d]", sqlca.sqlcode); FAIL(b);
}
/* ===== 6) 감사이력 적재부 (cm_log) ===== */
snprintf(msgbuf, sizeof(msgbuf), "ST_FUNDINSTR merch=%s biz=%s bank=%s acct=%s amount=%ld status=%s",
h_merch, h_bizdate, h_bank, h_acct, h_amount, h_status);
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, 8630); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); }
/* ===== 7) 재검증부 (적재행 재조회) ===== */
EXEC SQL SELECT amount INTO :h_chk FROM st_funding_instr WHERE instr_id = :h_instr;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8640); Bchg(b, T_MSG, 0, "지시 적재 재검증 실패", 0L);
userlog("[ST_FUNDINSTR] 재검증 실패 code=%d", sqlca.sqlcode); FAIL(b);
}
if (h_chk != h_amount) {
setl(b, T_RC, 8641); Bchg(b, T_MSG, 0, "지시 금액 불일치(보상 롤백)", 0L);
userlog("[ST_FUNDINSTR] 금액 불일치 chk=%ld amount=%ld", h_chk, h_amount); FAIL(b);
}
/* ===== 8) 응답부 (계약: T_AMT1=amount, T_STATUS=status, T_ID1=instr_id) ===== */
setl(b, T_AMT1, h_amount);
setl(b, T_AMT2, h_deposit);
Bchg(b, T_STATUS, 0, h_status, 0L);
setl(b, T_ID1, h_instr);
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
Bchg(b, T_STR1, 0, h_bank, 0L);
Bchg(b, T_STR2, 0, h_acct, 0L);
Bchg(b, T_SVCNAME, 0, "ST_FUNDINSTR", 0L);
setl(b, T_RC, 0);
/* ===== 9) 완료 로깅 ===== */
userlog("[자금연동] 펀딩 지시 완료 merch=%s biz=%s instr=%ld amount=%ld status=%s",
h_merch, h_bizdate, h_instr, h_amount, h_status);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */