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

@ -160,6 +160,10 @@
#include "ACQ_DISPUTE_ADVANCE.h"
#include "ACQ_PRESENT.h"
/* Wave J — 자금연동(DCC 다통화 환산) + 운영(매출전표 조회요청) */
#include "AC_DCC.h"
#include "ACQ_RETRIEVAL.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},
@ -261,6 +265,8 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"ACQ_DISPUTE_OPEN", ACQ_DISPUTE_OPEN},
{"ACQ_DISPUTE_ADVANCE", ACQ_DISPUTE_ADVANCE},
{"ACQ_PRESENT", ACQ_PRESENT},
{"AC_DCC", AC_DCC},
{"ACQ_RETRIEVAL", ACQ_RETRIEVAL},
{NULL, NULL}
};

View file

@ -0,0 +1,25 @@
/*
* ACQ_RETRIEVAL.h - ac ACQ_RETRIEVAL (copybook / record header).
* (retrieval request) /: .
* action=FULFILL FULFILLED(+fulfilled_at) ,
* OPEN (sla_due = today + 20).
* Request fields: T_PURCHASE_ID, T_STR1(reason action=FULFILL)
* Response fields: T_ID1(retr_id), T_STATUS
*/
#ifndef ACQ_RETRIEVAL_H
#define ACQ_RETRIEVAL_H
#include "ac_svc.h"
typedef struct {
long retr_id;
long purchase_id;
char merchant_id[64];
char reason[128];
char status[16]; /* OPEN/FULFILLED/EXPIRED */
char sla_due[16];
} ac_retrieval_rec_t;
void ACQ_RETRIEVAL(TPSVCINFO *p);
#endif /* ACQ_RETRIEVAL_H */

View file

@ -0,0 +1,117 @@
/*
* ACQ_RETRIEVAL.pgc - ac (매입) 모듈 서비스 ACQ_RETRIEVAL (one service = one file).
* 매출전표 조회요청(retrieval request) 생성/이행: 정식 채그백 직전 단계로, 발급사가
* 거래 증빙(매출전표/서명/수령증)을 요청하면 조회요청을 열고(OPEN, SLA=today+20),
* 가맹점이 증빙을 제출하면 이행(FULFILLED + fulfilled_at)한다.
* 처리단계: 입력검증 -> 매입 컨텍스트 조회 -> action 분기(FULFILL/신규) -> 본처리 ->
* 감사이력 -> 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). ac_svr 가 tpsvrinit 에서 광고.
* XA 격리: 조회요청/매입은 본 브랜치에서 조회 가능한 커밋행 기준으로 판정한다.
*/
#include "ACQ_RETRIEVAL.h"
/* ACQ_RETRIEVAL - 매출전표 조회요청 생성/이행. */
void ACQ_RETRIEVAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
char reason[128], msgbuf[220];
int fulfil = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_retr, h_chk;
char h_merch[64], h_reason[128], h_status[16], h_bizdate[16];
char h_cstatus[16], h_svc[24], h_msg[220];
EXEC SQL END DECLARE SECTION;
strcpy(h_svc, "ACQ_RETRIEVAL");
h_pid = h_retr = h_chk = 0;
strcpy(h_merch, "UNKNOWN"); strcpy(h_status, "OPEN");
strcpy(h_bizdate, "2026-07-19"); h_cstatus[0] = 0;
/* ===== 1) 입력 검증부 ===== */
h_pid = getl(b, T_PURCHASE_ID);
if (h_pid <= 0) {
setl(b, T_RC, 8801); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
userlog("[ACQ_RETRIEVAL] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b);
}
gets_(b, T_STR1, reason, sizeof(reason));
if (strcmp(reason, "FULFILL") == 0) fulfil = 1;
if (reason[0] == 0) strcpy(reason, "매출전표사본");
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
userlog("[운영] 조회요청 진입 pid=%ld action=%s", h_pid, fulfil ? "FULFILL" : reason);
/* ===== 2) 매입 컨텍스트 조회부 (가맹점/영업일) ===== */
EXEC SQL SELECT merchant_id, biz_date INTO :h_merch, :h_bizdate FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode == 100) {
strcpy(h_merch, "UNKNOWN");
if (h_bizdate[0] == 0) strcpy(h_bizdate, "2026-07-19");
userlog("[ACQ_RETRIEVAL] 매입 미조회 pid=%ld — 기본값으로 진행", h_pid);
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8810); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b);
}
/* ===== 3) action 분기부 (이행 / 신규 생성) ===== */
if (fulfil) {
/* 이행: 열린 조회요청 1건을 FULFILLED 로 전이 */
EXEC SQL SELECT retr_id INTO :h_retr FROM ac_retrieval
WHERE purchase_id = :h_pid AND status = 'OPEN' ORDER BY retr_id LIMIT 1;
if (sqlca.sqlcode == 100) {
Bchg(b, T_STATUS, 0, "NOOPEN", 0L);
setl(b, T_ID1, 0); Bchg(b, T_SVCNAME, 0, "ACQ_RETRIEVAL", 0L); setl(b, T_RC, 0);
userlog("[ACQ_RETRIEVAL] 이행 대상(OPEN) 없음 pid=%ld", h_pid);
OK(b);
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8811); Bchg(b, T_MSG, 0, "조회요청 조회 오류", 0L); FAIL(b);
}
strcpy(h_status, "FULFILLED");
EXEC SQL UPDATE ac_retrieval SET status = 'FULFILLED', fulfilled_at = now()
WHERE retr_id = :h_retr;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8820); Bchg(b, T_MSG, 0, "조회요청 이행 실패", 0L);
userlog("[ACQ_RETRIEVAL] FULFILL UPDATE 실패 [%d] retr=%ld", sqlca.sqlcode, h_retr); FAIL(b);
}
} else {
/* 신규: OPEN 조회요청 생성(SLA = today + 20) */
strcpy(h_status, "OPEN");
EXEC SQL SELECT nextval('ac_retrieval_seq') INTO :h_retr;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8821); Bchg(b, T_MSG, 0, "조회요청 시퀀스 실패", 0L); FAIL(b); }
EXEC SQL INSERT INTO ac_retrieval
(retr_id, purchase_id, merchant_id, reason, status, sla_due)
VALUES (:h_retr, :h_pid, :h_merch, :h_reason, 'OPEN', CURRENT_DATE + 20);
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8822); Bchg(b, T_MSG, 0, "조회요청 적재 실패", 0L);
userlog("[ACQ_RETRIEVAL] ac_retrieval INSERT 실패 [%d]", sqlca.sqlcode); FAIL(b);
}
}
/* ===== 4) 감사이력 적재부 (cm_log) ===== */
snprintf(msgbuf, sizeof(msgbuf), "ACQ_RETRIEVAL pid=%ld retr=%ld merch=%s status=%s reason=%s",
h_pid, h_retr, h_merch, h_status, h_reason);
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, 8830); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); }
/* ===== 5) 재검증부 (적재/전이 상태 재조회) ===== */
EXEC SQL SELECT retr_id INTO :h_chk FROM ac_retrieval WHERE retr_id = :h_retr AND status = :h_status;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8840); Bchg(b, T_MSG, 0, "조회요청 재검증 실패", 0L);
userlog("[ACQ_RETRIEVAL] 재검증 실패 code=%d retr=%ld status=%s", sqlca.sqlcode, h_retr, h_status); FAIL(b);
}
/* ===== 6) 응답부 (계약: T_ID1=retr_id, T_STATUS) ===== */
setl(b, T_ID1, h_retr);
Bchg(b, T_STATUS, 0, h_status, 0L);
setl(b, T_PURCHASE_ID, h_pid);
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
Bchg(b, T_REASON, 0, h_reason, 0L);
Bchg(b, T_SVCNAME, 0, "ACQ_RETRIEVAL", 0L);
setl(b, T_RC, 0);
/* ===== 7) 완료 로깅 ===== */
userlog("[운영] 조회요청 완료 pid=%ld retr=%ld status=%s merch=%s", h_pid, h_retr, h_status, h_merch);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

26
app/src/ac/svc/AC_DCC.h Normal file
View file

@ -0,0 +1,26 @@
/*
* AC_DCC.h - ac AC_DCC (copybook / record header).
* DCC(Dynamic Currency Conversion) :
* (USD/JPY/EUR/CNY) + (3%) ac_dcc .
* Request fields: T_PURCHASE_ID, T_STR1(txn_ccy), T_AMOUNT()
* Response fields: T_AMT1(txn_amount ), T_STR2(txn_ccy), T_ARG1(fx_rate )
*/
#ifndef AC_DCC_H
#define AC_DCC_H
#include "ac_svc.h"
typedef struct {
long dcc_id;
long purchase_id;
char base_ccy[8]; /* KRW */
char txn_ccy[8]; /* USD/JPY/EUR/CNY */
long base_amount; /* 원화 금액 */
long txn_amount; /* 외화 금액(정수부) */
long fx_rate; /* 고정환율(정수부) */
long markup_bps; /* DCC 마크업 */
} ac_dcc_rec_t;
void AC_DCC(TPSVCINFO *p);
#endif /* AC_DCC_H */

131
app/src/ac/svc/AC_DCC.pgc Normal file
View file

@ -0,0 +1,131 @@
/*
* AC_DCC.pgc - ac (매입) 모듈 서비스 AC_DCC (one service = one file).
* DCC(Dynamic Currency Conversion) 다통화 환산: 해외카드 소지자가 원화가맹점에서
* 자국통화 결제를 선택할 때, 원화 매입금액을 승인시점 고정환율 + DCC 마크업(3%)
* 으로 카드소지자 통화(USD/JPY/EUR/CNY)로 환산해 ac_dcc 에 적재한다.
* 환율(원/외화 1단위): USD 1330, JPY 8.9, EUR 1440, CNY 185 (스케일 x10 정수보관).
* txn_amount = base_amount / rate * (1 + markup) — 정수 절삭 없이 NUMERIC 연산.
* 처리단계: 입력검증 -> 매입 컨텍스트 조회 -> 통화/환율 결정 -> 환산 -> 본처리(INSERT)
* -> 감사이력 -> 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). ac_svr 가 tpsvrinit 에서 광고.
*/
#include "AC_DCC.h"
/* AC_DCC - DCC 다통화 환산(승인시점 환율고정 + 마크업). */
void AC_DCC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
char ccy[8], msgbuf[220];
long rate10 = 0, txn_int = 0, fx_int = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_base, h_rate10, h_markup, h_dcc, h_txn_i, h_orig;
char h_ccy[8], h_base_ccy[8], h_bizdate[16], h_status[16], h_svc[24], h_msg[220];
EXEC SQL END DECLARE SECTION;
strcpy(h_svc, "AC_DCC");
h_pid = h_base = h_dcc = h_txn_i = h_orig = 0;
h_markup = 300; strcpy(h_base_ccy, "KRW"); strcpy(h_status, "CONVERTED");
strcpy(h_bizdate, "2026-07-19");
/* ===== 1) 입력 검증부 ===== */
h_pid = getl(b, T_PURCHASE_ID);
if (h_pid <= 0) {
setl(b, T_RC, 8701); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
userlog("[AC_DCC] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b);
}
gets_(b, T_STR1, ccy, sizeof(ccy));
if (ccy[0] == 0) strcpy(ccy, "USD");
h_base = getl(b, T_AMOUNT);
/* 통화별 고정환율(스케일 x10): 문자열 비교로 결정(캐스팅 없음). */
if (strcmp(ccy, "USD") == 0) rate10 = 13300;
else if (strcmp(ccy, "JPY") == 0) rate10 = 89;
else if (strcmp(ccy, "EUR") == 0) rate10 = 14400;
else if (strcmp(ccy, "CNY") == 0) rate10 = 1850;
else {
setl(b, T_RC, 8702); Bchg(b, T_MSG, 0, "지원하지 않는 통화(txn_ccy)", 0L);
userlog("[AC_DCC] 미지원 통화 ccy=[%s]", ccy); FAIL(b);
}
strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0;
h_rate10 = rate10;
userlog("[다통화] DCC 환산 진입 pid=%ld ccy=%s base=%ld rate10=%ld", h_pid, h_ccy, h_base, h_rate10);
/* ===== 2) 매입 컨텍스트 조회부 (원금/영업일 보정) ===== */
EXEC SQL SELECT amount, biz_date INTO :h_orig, :h_bizdate FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode == 100) {
/* 체인/미커밋 매입일 수 있어 실패 처리하지 않는다(RECONCILE 격리 규율). */
if (h_bizdate[0] == 0) strcpy(h_bizdate, "2026-07-19");
userlog("[AC_DCC] 매입 미조회 pid=%ld — 전문/기본값으로 진행", h_pid);
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8710); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L); FAIL(b);
}
/* 원화금액 미지정 시 원 매입금액을 환산 대상으로 사용 */
if (h_base <= 0) h_base = h_orig;
if (h_base <= 0) {
setl(b, T_RC, 8703); Bchg(b, T_MSG, 0, "환산 대상 원화 금액 없음", 0L);
userlog("[AC_DCC] 환산 금액 0 pid=%ld", h_pid); FAIL(b);
}
/* ===== 3) 환산부 (외화금액·환율 정수부 산출) ===== */
/* txn = base/rate*(1+markup) = base*(10000+markup)/(rate10*1000) */
txn_int = h_base * (10000 + h_markup) / (h_rate10 * 1000);
fx_int = h_rate10 / 10;
userlog("[AC_DCC] 환산 base=%ld ccy=%s rate=%ld.%ld markup=%ld -> txn≈%ld %s",
h_base, h_ccy, fx_int, h_rate10 % 10, h_markup, txn_int, h_ccy);
/* ===== 4) 본처리부 (ac_dcc INSERT — NUMERIC 정밀연산) ===== */
EXEC SQL SELECT nextval('ac_dcc_seq') INTO :h_dcc;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 8720); Bchg(b, T_MSG, 0, "DCC 시퀀스 실패", 0L); FAIL(b); }
EXEC SQL INSERT INTO ac_dcc
(dcc_id, purchase_id, base_ccy, txn_ccy, base_amount, txn_amount, fx_rate, markup_bps, opted_in)
VALUES (:h_dcc, :h_pid, :h_base_ccy, :h_ccy, :h_base,
:h_base * (10000 + :h_markup) / (:h_rate10 * 1000.0),
:h_rate10 / 10.0, :h_markup, true);
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8721); Bchg(b, T_MSG, 0, "DCC 적재 실패", 0L);
userlog("[AC_DCC] ac_dcc INSERT 실패 [%d]", sqlca.sqlcode); FAIL(b);
}
/* ===== 5) 감사이력 적재부 (cm_log) ===== */
snprintf(msgbuf, sizeof(msgbuf), "AC_DCC pid=%ld base=%ld KRW -> %ld %s (rate=%ld.%ld markup=%ldbps)",
h_pid, h_base, txn_int, h_ccy, fx_int, h_rate10 % 10, h_markup);
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, 8730); Bchg(b, T_MSG, 0, "감사 로그 적재 실패", 0L); FAIL(b); }
/* ===== 6) 재검증부 (적재행 외화금액 정수부 재조회) ===== */
EXEC SQL SELECT floor(txn_amount) INTO :h_txn_i FROM ac_dcc WHERE dcc_id = :h_dcc;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8740); Bchg(b, T_MSG, 0, "DCC 적재 재검증 실패", 0L);
userlog("[AC_DCC] 재검증 실패 code=%d", sqlca.sqlcode); FAIL(b);
}
if (h_txn_i != txn_int) {
/* 반올림 경계 1 이내 허용, 그 외 불일치는 실패 처리 */
long d = h_txn_i - txn_int; if (d < 0) d = -d;
if (d > 1) {
setl(b, T_RC, 8741); Bchg(b, T_MSG, 0, "DCC 환산 불일치(보상 롤백)", 0L);
userlog("[AC_DCC] 환산 불일치 db=%ld calc=%ld", h_txn_i, txn_int); FAIL(b);
}
txn_int = h_txn_i;
}
/* ===== 7) 응답부 (계약: T_AMT1=txn_amount, T_STR2=txn_ccy, T_ARG1=fx_rate) ===== */
setl(b, T_AMT1, txn_int);
Bchg(b, T_STR2, 0, h_ccy, 0L);
setl(b, T_ARG1, fx_int);
setl(b, T_AMT2, h_base);
setl(b, T_ID1, h_dcc);
Bchg(b, T_STATUS, 0, h_status, 0L);
Bchg(b, T_CCY, 0, h_ccy, 0L);
Bchg(b, T_SVCNAME, 0, "AC_DCC", 0L);
setl(b, T_RC, 0);
/* ===== 8) 완료 로깅 ===== */
userlog("[다통화] DCC 환산 완료 pid=%ld dcc=%ld base=%ld -> %ld %s", h_pid, h_dcc, h_base, txn_int, h_ccy);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -94,6 +94,10 @@
#include "RISK_SCORE_INQ.h"
#include "APPROVAL_RATE.h"
#include "AUTH_DUP_CHK.h"
/* --- Wave J: 카드보안 승인 3본 (au) --- */
#include "TOKENIZE.h"
#include "AUTH_3DS.h"
#include "AUTH_VERIFY.h"
/* --- scale-out services (승인/한도 심화) --- */
#include "LIMIT_MONTH_CHK.h"
#include "LIMIT_MONTH_INQ.h"
@ -194,6 +198,9 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"MERCH_AUTH_RANK", MERCH_AUTH_RANK}, {"CARD_DAILY_RPT", CARD_DAILY_RPT},
{"RISK_SCORE_INQ", RISK_SCORE_INQ}, {"APPROVAL_RATE", APPROVAL_RATE},
{"AUTH_DUP_CHK", AUTH_DUP_CHK},
/* --- Wave J: 카드보안 승인 3본 (au) --- */
{"TOKENIZE", TOKENIZE}, {"AUTH_3DS", AUTH_3DS},
{"AUTH_VERIFY", AUTH_VERIFY},
/* --- scale-out services (승인/한도 심화) --- */
{"LIMIT_MONTH_CHK", LIMIT_MONTH_CHK}, {"LIMIT_MONTH_INQ", LIMIT_MONTH_INQ},
{"LIMIT_PERTX_CHK", LIMIT_PERTX_CHK}, {"LIMIT_CUMUL_INQ", LIMIT_CUMUL_INQ},

24
app/src/au/svc/AUTH_3DS.h Normal file
View file

@ -0,0 +1,24 @@
/*
* AUTH_3DS.h - au AUTH_3DS (copybook / record header).
* : 3DS/SCA · (ECI auth_status/liability, ).
* Request fields: T_PURCHASE_ID( ID), T_STR1(eci: 05/06/07)
* Response fields: T_STATUS(auth_status: Y/A/N/U), T_STR2(liability: ISSUER/MERCHANT),
* T_ID1(tds_id), T_RC, T_MSG
*/
#ifndef AUTH_3DS_H
#define AUTH_3DS_H
#include "au_svc.h"
typedef struct {
long tds_id;
long purchase_id;
char version[16];
char eci[8];
char auth_status[8]; /* Y=인증 A=시도 N=실패 U=불가 */
char liability[16]; /* ISSUER(전환됨) / MERCHANT */
} au_3ds_rec_t;
void AUTH_3DS(TPSVCINFO *p);
#endif /* AUTH_3DS_H */

124
app/src/au/svc/AUTH_3DS.pgc Normal file
View file

@ -0,0 +1,124 @@
/*
* AUTH_3DS.pgc - au 모듈 서비스 AUTH_3DS (one service = one file).
* 3DS/SCA 비대면 인증결과 기록·판정: 입력 ECI 를 인증상태(auth_status)와
* 책임주체(liability)로 매핑하여 au_3ds 원장에 적재한다.
* ECI 05 → Y(완전인증) / ISSUER(책임전환)
* ECI 06 → A(시도) / ISSUER(책임전환)
* ECI 07 → N(미인증) / MERCHANT(가맹점 책임)
* 그 외 → U(불가) / MERCHANT
* 처리단계: 입력검증 -> 매입/기존인증 조회 -> ECI 판정분기 -> 본처리(INSERT) ->
* 적재 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). au_svr 가 tpsvrinit 에서 광고.
* XA 격리: 체인 내 미커밋 매입은 이 브랜치에서 안 보일 수 있으므로 조회 미존재(100)는
* 정상 흐름으로 간주하고 FAIL 하지 않는다(owner-writes 규율, RECONCILE 패턴).
*/
#include "AUTH_3DS.h"
/* AUTH_3DS - 3DS/SCA 인증결과 기록·판정. */
void AUTH_3DS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *svcname = "AUTH_3DS";
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
long rc = 0;
char inbuf[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_eci[8];
char h_astat[8];
char h_liab[16];
char h_ver[16];
long h_tds;
long h_prior;
long h_vcnt;
long h_pcnt;
EXEC SQL END DECLARE SECTION;
memset(inbuf, 0, sizeof(inbuf));
h_pid = 0; h_tds = 0; h_prior = 0; h_vcnt = 0; h_pcnt = 0;
memset(h_eci, 0, sizeof(h_eci)); memset(h_astat, 0, sizeof(h_astat));
memset(h_liab, 0, sizeof(h_liab)); memset(h_ver, 0, sizeof(h_ver));
strcpy(h_ver, "2.2.0");
userlog("[%s] 진입: 3DS/SCA 인증결과 기록·판정", svcname);
/* ===== 1) 입력 검증부 ===== */
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_STR1, inbuf, sizeof(inbuf));
if (h_pid <= 0) {
setl(b, T_RC, 8401L); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
userlog("[%s] 입력검증 실패: pid=%ld", svcname, h_pid); FAIL(b);
}
if (inbuf[0] == 0) {
setl(b, T_RC, 8402L); Bchg(b, T_MSG, 0, "ECI 필수 누락", 0L);
userlog("[%s] 입력검증 실패: ECI 누락 pid=%ld", svcname, h_pid); FAIL(b);
}
strncpy(h_eci, inbuf, sizeof(h_eci) - 1); h_eci[sizeof(h_eci) - 1] = 0;
/* ===== 2) 조회부 (매입 존재 + 기존 인증/검증 현황) =====
* 매입 미존재(100)는 체인 신규건일 수 있어 정상 처리. */
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8410L); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L);
userlog("[%s] 매입 조회 오류 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
if (h_pcnt == 0)
userlog("[%s] 매입 미커밋/미존재 pid=%ld — 인증만 기록", svcname, h_pid);
EXEC SQL SELECT count(*) INTO :h_prior FROM au_3ds WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_prior = 0; }
EXEC SQL SELECT count(*) INTO :h_vcnt FROM au_verify WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_vcnt = 0; }
if (h_prior > 0)
userlog("[%s] 기존 3DS 인증 %ld건 존재 pid=%ld — 재인증 기록", svcname, h_prior, h_pid);
/* ===== 3) ECI 판정 분기부 (auth_status / liability) ===== */
if (strcmp(h_eci, "05") == 0) {
strcpy(h_astat, "Y"); strcpy(h_liab, "ISSUER"); rc = 0;
} else if (strcmp(h_eci, "06") == 0) {
strcpy(h_astat, "A"); strcpy(h_liab, "ISSUER"); rc = 0;
} else if (strcmp(h_eci, "07") == 0) {
strcpy(h_astat, "N"); strcpy(h_liab, "MERCHANT"); rc = 2;
userlog("[%s] 미인증(ECI07) — 책임 가맹점 pid=%ld", svcname, h_pid);
} else {
strcpy(h_astat, "U"); strcpy(h_liab, "MERCHANT"); rc = 1;
userlog("[%s] 미지원 ECI=[%s] — 불가(U)/가맹점 pid=%ld", svcname, h_eci, h_pid);
}
/* ===== 4) 본처리부 (au_3ds 채번·INSERT) ===== */
EXEC SQL SELECT nextval('au_3ds_seq') INTO :h_tds;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8420L); Bchg(b, T_MSG, 0, "3DS 시퀀스 채번 실패", 0L);
userlog("[%s] 시퀀스 채번 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
EXEC SQL INSERT INTO au_3ds
(tds_id, purchase_id, version, eci, auth_status, liability)
VALUES (:h_tds, :h_pid, :h_ver, :h_eci, :h_astat, :h_liab);
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8421L); Bchg(b, T_MSG, 0, "3DS 인증결과 적재 실패", 0L);
userlog("[%s] au_3ds 적재 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
/* ===== 5) 적재 재검증부 (기록행 재조회) ===== */
EXEC SQL SELECT auth_status, liability INTO :h_astat, :h_liab
FROM au_3ds WHERE tds_id = :h_tds;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8430L); Bchg(b, T_MSG, 0, "3DS 적재 재검증 실패", 0L);
userlog("[%s] 재검증 실패 tds=%ld code=%d", svcname, h_tds, sqlca.sqlcode); FAIL(b);
}
/* ===== 6) 응답부 ===== */
Bchg(b, T_STATUS, 0, h_astat, 0L); /* auth_status: Y/A/N/U */
Bchg(b, T_STR2, 0, h_liab, 0L); /* liability: ISSUER/MERCHANT */
setl(b, T_ID1, h_tds);
setl(b, T_RC, rc);
Bchg(b, T_MSG, 0,
(strcmp(h_liab, "ISSUER") == 0 ? "3DS 인증 완료(책임전환)" : "3DS 미전환(가맹점 책임)"),
0L);
/* ===== 7) 완료 로깅 ===== */
userlog("[%s] 완료: pid=%ld eci=%s status=%s liability=%s tds=%ld rc=%ld",
svcname, h_pid, h_eci, h_astat, h_liab, h_tds, rc);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,23 @@
/*
* AUTH_VERIFY.h - au AUTH_VERIFY (copybook / record header).
* : AVS/CVV + (risk_score APPROVE/REVIEW/DECLINE).
* Request fields: T_PURCHASE_ID( ID), T_STR1(avs_code), T_STR2(cvv_code)
* Response fields: T_STATUS(decision), T_ARG1(risk_score), T_ID1(vrf_id), T_RC, T_MSG
*/
#ifndef AUTH_VERIFY_H
#define AUTH_VERIFY_H
#include "au_svc.h"
typedef struct {
long vrf_id;
long purchase_id;
char avs_code[8]; /* Y/A/Z/N/U */
char cvv_code[8]; /* M/N/P/U */
long risk_score; /* 0~999 */
char decision[16]; /* APPROVE / REVIEW / DECLINE */
} au_verify_rec_t;
void AUTH_VERIFY(TPSVCINFO *p);
#endif /* AUTH_VERIFY_H */

View file

@ -0,0 +1,138 @@
/*
* AUTH_VERIFY.pgc - au 모듈 서비스 AUTH_VERIFY (one service = one file).
* AVS/CVV 검증 + 리스크 판정: 주소검증(AVS)·카드검증값(CVV) 코드로 리스크 점수를
* 산출하고, 임계값으로 승인판정(decision)을 내려 au_verify 원장에 적재한다.
* risk_score > 900 → DECLINE, > 750 → REVIEW, 그 외 → APPROVE.
* AVS/CVV 응답 품질이 나쁠수록 가점(위험↑). 3DS 책임미전환(MERCHANT)이면 가중.
* 처리단계: 입력검증 -> 매입/기존검증/3DS 조회 -> 리스크 산출 -> 판정분기 ->
* 본처리(INSERT) -> 적재 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). au_svr 가 tpsvrinit 에서 광고.
* XA 격리: 체인 내 미커밋 매입은 안 보일 수 있어 미존재(100)는 정상 처리한다.
*/
#include "AUTH_VERIFY.h"
/* AUTH_VERIFY - AVS/CVV 검증 + 리스크 판정. */
void AUTH_VERIFY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *svcname = "AUTH_VERIFY";
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
long rc = 0, risk = 0;
char avsbuf[8], cvvbuf[8];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_avs[8];
char h_cvv[8];
long h_score;
char h_dec[16];
long h_vrf;
long h_prior;
long h_pcnt;
char h_liab[16];
EXEC SQL END DECLARE SECTION;
memset(avsbuf, 0, sizeof(avsbuf)); memset(cvvbuf, 0, sizeof(cvvbuf));
h_pid = 0; h_score = 0; h_vrf = 0; h_prior = 0; h_pcnt = 0;
memset(h_avs, 0, sizeof(h_avs)); memset(h_cvv, 0, sizeof(h_cvv));
memset(h_dec, 0, sizeof(h_dec)); memset(h_liab, 0, sizeof(h_liab));
strcpy(h_liab, "NONE");
userlog("[%s] 진입: AVS/CVV 검증 + 리스크 판정", svcname);
/* ===== 1) 입력 검증부 ===== */
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_STR1, avsbuf, sizeof(avsbuf));
gets_(b, T_STR2, cvvbuf, sizeof(cvvbuf));
if (h_pid <= 0) {
setl(b, T_RC, 8501L); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
userlog("[%s] 입력검증 실패: pid=%ld", svcname, h_pid); FAIL(b);
}
if (avsbuf[0] == 0) strcpy(avsbuf, "U"); /* 미제공 → 불가(U) */
if (cvvbuf[0] == 0) strcpy(cvvbuf, "U");
strncpy(h_avs, avsbuf, sizeof(h_avs) - 1); h_avs[sizeof(h_avs) - 1] = 0;
strncpy(h_cvv, cvvbuf, sizeof(h_cvv) - 1); h_cvv[sizeof(h_cvv) - 1] = 0;
/* ===== 2) 조회부 (매입 존재 + 기존 검증/3DS 책임 현황) ===== */
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8510L); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L);
userlog("[%s] 매입 조회 오류 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
if (h_pcnt == 0)
userlog("[%s] 매입 미커밋/미존재 pid=%ld — 검증만 기록", svcname, h_pid);
EXEC SQL SELECT count(*) INTO :h_prior FROM au_verify WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_prior = 0; }
EXEC SQL SELECT coalesce(max(liability), 'NONE') INTO :h_liab
FROM au_3ds WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { strcpy(h_liab, "NONE"); }
/* ===== 3) 리스크 산출부 (AVS/CVV 품질 → 가점) =====
* AVS: Y 일치=0, A 부분=50, Z 우편만=80, N 불일치=200, U 불가=150.
* CVV: M 일치=0, N 불일치=400, P 미처리=150, U 불가=150.
* 3DS 책임 미전환(MERCHANT)면 +100. pid 기반 소폭 분산 후 0~999 로 클램프. */
if (strcmp(h_avs, "Y") == 0) risk += 0;
else if (strcmp(h_avs, "A") == 0) risk += 50;
else if (strcmp(h_avs, "Z") == 0) risk += 80;
else if (strcmp(h_avs, "N") == 0) risk += 200;
else risk += 150; /* U 및 기타 */
if (strcmp(h_cvv, "M") == 0) risk += 0;
else if (strcmp(h_cvv, "N") == 0) risk += 400;
else if (strcmp(h_cvv, "P") == 0) risk += 150;
else risk += 150; /* U 및 기타 */
if (strcmp(h_liab, "MERCHANT") == 0) {
risk += 100;
userlog("[%s] 3DS 책임 미전환(MERCHANT) — 위험 가중 pid=%ld", svcname, h_pid);
}
risk += (long)(h_pid % 50); /* 소폭 분산 */
if (risk < 0) risk = 0;
if (risk > 999) risk = 999;
h_score = risk;
/* ===== 4) 판정 분기부 (임계값) ===== */
if (risk > 900) {
strcpy(h_dec, "DECLINE"); rc = 2;
} else if (risk > 750) {
strcpy(h_dec, "REVIEW"); rc = 1;
} else {
strcpy(h_dec, "APPROVE"); rc = 0;
}
userlog("[%s] 리스크 산출 pid=%ld avs=%s cvv=%s liab=%s score=%ld dec=%s",
svcname, h_pid, h_avs, h_cvv, h_liab, h_score, h_dec);
/* ===== 5) 본처리부 (au_verify 채번·INSERT) ===== */
EXEC SQL SELECT nextval('au_verify_seq') INTO :h_vrf;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8520L); Bchg(b, T_MSG, 0, "검증 시퀀스 채번 실패", 0L);
userlog("[%s] 시퀀스 채번 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
EXEC SQL INSERT INTO au_verify
(vrf_id, purchase_id, avs_code, cvv_code, risk_score, decision)
VALUES (:h_vrf, :h_pid, :h_avs, :h_cvv, :h_score, :h_dec);
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8521L); Bchg(b, T_MSG, 0, "검증결과 적재 실패", 0L);
userlog("[%s] au_verify 적재 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
/* ===== 6) 적재 재검증부 (기록행 재조회) ===== */
EXEC SQL SELECT decision, risk_score INTO :h_dec, :h_score
FROM au_verify WHERE vrf_id = :h_vrf;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8530L); Bchg(b, T_MSG, 0, "검증 적재 재검증 실패", 0L);
userlog("[%s] 재검증 실패 vrf=%ld code=%d", svcname, h_vrf, sqlca.sqlcode); FAIL(b);
}
/* ===== 7) 응답부 ===== */
Bchg(b, T_STATUS, 0, h_dec, 0L); /* decision: APPROVE/REVIEW/DECLINE */
setl(b, T_ARG1, h_score); /* risk_score */
setl(b, T_ID1, h_vrf);
setl(b, T_RC, rc);
Bchg(b, T_MSG, 0,
(rc == 2 ? "리스크 거절" : (rc == 1 ? "리스크 검토대상" : "검증 통과")), 0L);
/* ===== 8) 완료 로깅 ===== */
userlog("[%s] 완료: pid=%ld decision=%s score=%ld vrf=%ld rc=%ld",
svcname, h_pid, h_dec, h_score, h_vrf, rc);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

24
app/src/au/svc/TOKENIZE.h Normal file
View file

@ -0,0 +1,24 @@
/*
* TOKENIZE.h - au TOKENIZE (copybook / record header).
* : PAN / (PCI PAN /).
* Request fields: T_KEY1( )
* Response fields: T_STR1(token), T_STR2(pan_masked), T_STATUS(NEW/FOUND/NOTFOUND),
* T_ID1( 퀀 audit id), T_RC, T_MSG
*/
#ifndef TOKENIZE_H
#define TOKENIZE_H
#include "au_svc.h"
typedef struct {
char token[40]; /* 표면 토큰 (비민감) */
char pan_masked[40]; /* 4111-****-****-1111 */
char pan_hash[80]; /* SHA-256(PAN) hex — 원문 불가역 */
char bin[12];
char last4[8];
char status[12]; /* NEW / FOUND / NOTFOUND */
} au_tokenize_rec_t;
void TOKENIZE(TPSVCINFO *p);
#endif /* TOKENIZE_H */

143
app/src/au/svc/TOKENIZE.pgc Normal file
View file

@ -0,0 +1,143 @@
/*
* TOKENIZE.pgc - au 모듈 서비스 TOKENIZE (one service = one file).
* PAN 토큰화 조회/발급: 입력 PAN 을 SHA-256 해시로 card_vault 에서 조회하고,
* 있으면 기존 토큰을, 없으면 신규 토큰('TKN-'||md5 앞12)을 발급/적재한다.
* PCI 스코프 축소 규율: 원문 PAN 은 저장하지 않고(마스킹+해시만), 응답에도
* 절대 싣지 않는다. 응답은 토큰과 마스킹 문자열, 발급 상태만 반환한다.
* 처리단계: 입력검증 -> 해시산출 -> 볼트조회 -> 업무규칙분기(원문/마스킹) ->
* 본처리(신규발급 INSERT) -> 적재 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). au_svr 가 tpsvrinit 에서 광고.
* 캐스팅 규율: 호스트변수 date 캐스팅 금지. 해시는 convert_to(...,'UTF8') 로
* bytea 변환하여 시드(sha256(card_no::bytea))와 동일 해시를 얻는다(ASCII 동치).
*/
#include "TOKENIZE.h"
/* TOKENIZE - PAN 토큰화 조회/발급 (원문 PAN 미응답). */
void TOKENIZE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *svcname = "TOKENIZE";
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
long rc = 0, is_pan = 0, i = 0, ndigit = 0, slen = 0;
char inbuf[48];
EXEC SQL BEGIN DECLARE SECTION;
char h_card[48]; /* 입력 원문/마스킹 — 응답에 원문으로 절대 싣지 않음 */
char h_token[48];
char h_masked[48];
char h_hash[80];
long h_found;
long h_tokseq;
EXEC SQL END DECLARE SECTION;
memset(inbuf, 0, sizeof(inbuf));
memset(h_card, 0, sizeof(h_card)); memset(h_token, 0, sizeof(h_token));
memset(h_masked, 0, sizeof(h_masked)); memset(h_hash, 0, sizeof(h_hash));
h_found = 0; h_tokseq = 0;
userlog("[%s] 진입: PAN 토큰화 조회/발급 (원문 미저장/미응답)", svcname);
/* ===== 1) 입력 검증부 ===== */
gets_(b, T_KEY1, inbuf, sizeof(inbuf));
if (inbuf[0] == 0) {
setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L);
userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b);
}
strncpy(h_card, inbuf, sizeof(h_card) - 1); h_card[sizeof(h_card) - 1] = 0;
slen = (long)strlen(h_card);
if (slen < 4) {
setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "카드번호 형식 오류(길이 부족)", 0L);
userlog("[%s] 입력검증 실패: 길이 부족 len=%ld", svcname, slen); FAIL(b);
}
/* ===== 2) 입력 유형 판정부 (원문 PAN vs 마스킹) =====
* 전부 숫자이고 길이 12~19 이면 원문 PAN 으로 보고 신규발급을 허용한다.
* 그 외(대시/별표 포함)는 마스킹 문자열로 보고 조회만 수행한다. */
for (i = 0, ndigit = 0; i < slen; i++) {
if (h_card[i] >= '0' && h_card[i] <= '9') ndigit++;
}
is_pan = (ndigit == slen && slen >= 12 && slen <= 19) ? 1 : 0;
userlog("[%s] 입력유형 is_pan=%ld len=%ld digits=%ld", svcname, is_pan, slen, ndigit);
/* ===== 3) 해시 산출부 (원문 불가역 SHA-256) =====
* 시드(97c)는 sha256(card_no::bytea) 로 적재했다. convert_to(text,'UTF8') 는
* ASCII 숫자열에 대해 동일 bytea 를 주므로 같은 해시로 조회가 성립한다. */
EXEC SQL SELECT encode(sha256(convert_to(:h_card, 'UTF8')), 'hex') INTO :h_hash;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8810L); Bchg(b, T_MSG, 0, "해시 산출 실패", 0L);
userlog("[%s] 해시 산출 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
/* ===== 4) 볼트 조회부 (해시 일치 또는 마스킹 일치) ===== */
EXEC SQL SELECT token, pan_masked INTO :h_token, :h_masked
FROM card_vault
WHERE pan_hash = :h_hash OR pan_masked = :h_card
LIMIT 1;
if (sqlca.sqlcode == 100) {
h_found = 0;
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8811L); Bchg(b, T_MSG, 0, "토큰볼트 조회 오류", 0L);
userlog("[%s] 볼트 조회 오류 [%d]", svcname, sqlca.sqlcode); FAIL(b);
} else {
h_found = 1;
}
/* ===== 5) 업무규칙 분기부 ===== */
if (h_found) {
/* 이미 토큰화된 카드: 기존 토큰 반환 */
strcpy(inbuf, "FOUND"); rc = 0;
userlog("[%s] 기존 토큰 조회 성공 token=%s", svcname, h_token);
} else if (!is_pan) {
/* 마스킹 입력인데 미발견: 원문이 없어 발급 불가 — 조회 미스로 응답 */
strcpy(inbuf, "NOTFOUND"); rc = 1;
strncpy(h_masked, h_card, sizeof(h_masked) - 1); /* 입력이 이미 마스킹(비민감) */
h_masked[sizeof(h_masked) - 1] = 0;
h_token[0] = 0;
userlog("[%s] 마스킹 입력 미발견 — 발급 보류 rc=1", svcname);
} else {
/* ===== 6) 본처리부 (신규 토큰 발급 INSERT) ===== */
strcpy(inbuf, "NEW"); rc = 0;
EXEC SQL SELECT nextval('card_token_seq') INTO :h_tokseq; /* audit 채번 */
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8820L); Bchg(b, T_MSG, 0, "토큰 시퀀스 채번 실패", 0L);
userlog("[%s] 시퀀스 채번 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
EXEC SQL INSERT INTO card_vault
(token, pan_masked, pan_hash, bin, last4)
VALUES ('TKN-' || substr(md5(:h_card), 1, 12),
substr(:h_card, 1, 4) || '-****-****-' || right(:h_card, 4),
:h_hash, substr(:h_card, 1, 6), right(:h_card, 4))
RETURNING token, pan_masked INTO :h_token, :h_masked;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8821L); Bchg(b, T_MSG, 0, "토큰볼트 적재 실패", 0L);
userlog("[%s] 볼트 적재 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
userlog("[%s] 신규 토큰 발급 seq=%ld token=%s", svcname, h_tokseq, h_token);
}
/* ===== 7) 적재 재검증부 (발급/조회 결과 정합성) ===== */
if (h_token[0]) {
EXEC SQL SELECT count(*) INTO :h_found FROM card_vault WHERE token = :h_token;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8830L); Bchg(b, T_MSG, 0, "토큰 재검증 조회 오류", 0L); FAIL(b);
}
if (h_found <= 0) {
setl(b, T_RC, 8831L); Bchg(b, T_MSG, 0, "토큰 재검증 실패(적재행 미확인)", 0L);
userlog("[%s] 재검증 실패 token=%s", svcname, h_token); FAIL(b);
}
}
/* ===== 8) 응답부 (토큰/마스킹/상태만 — 원문 PAN 미포함) ===== */
Bchg(b, T_STR1, 0, h_token, 0L); /* token (비민감) */
Bchg(b, T_STR2, 0, h_masked, 0L); /* pan_masked (비민감) */
Bchg(b, T_STATUS, 0, inbuf, 0L); /* NEW / FOUND / NOTFOUND */
if (h_tokseq > 0) setl(b, T_ID1, h_tokseq);
setl(b, T_RC, rc);
if (rc == 0) Bchg(b, T_MSG, 0, "토큰화 정상", 0L);
/* ===== 9) 완료 로깅 ===== */
userlog("[%s] 완료: status=%s rc=%ld token=%s masked=%s",
svcname, inbuf, rc, h_token, h_masked);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

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