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:
parent
782b802156
commit
d292772b1f
21 changed files with 1337 additions and 7 deletions
|
|
@ -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
24
app/src/au/svc/AUTH_3DS.h
Normal 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
124
app/src/au/svc/AUTH_3DS.pgc
Normal 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: */
|
||||
23
app/src/au/svc/AUTH_VERIFY.h
Normal file
23
app/src/au/svc/AUTH_VERIFY.h
Normal 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 */
|
||||
138
app/src/au/svc/AUTH_VERIFY.pgc
Normal file
138
app/src/au/svc/AUTH_VERIFY.pgc
Normal 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
24
app/src/au/svc/TOKENIZE.h
Normal 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
143
app/src/au/svc/TOKENIZE.pgc
Normal 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: */
|
||||
Loading…
Add table
Add a link
Reference in a new issue