Wave K: 잔여 패리티 갭 완결 (패리티 80%→~87%)
패리티 분석의 남은 갭(온보딩·KYC·3자대사·ISO전문·계정체계·비기능)을 마무리. ■ 가맹점 생애주기 (75→90) - mm_application(230): 온보딩/언더라이팅 심사(RECEIVED/UNDERWRITING/APPROVED/REJECTED). - mm_kyc(1,150): KYC/AML 5종(IDENTITY/SANCTION/PEP/AML/CREDIT). - 서비스 MM_ONBOARD(상태기계+MID발급, 부적법전이 거부)·MM_KYC_CHECK. ■ 원장·회계·대사 (80→90) - lg_account(10): 계정과목 — suspense(미결)/clearing(정산대기) 분리. - rc_3way(16,185): 3자대사 카드망↔내부원장↔은행입금. 서비스 RC_3WAY. ■ 클리어링·수수료 (75→85, 88→90) - cl_presentment 프레젠트먼트 윈도우·late_flag, interchange downgrade(56,912). - ISO8583 취소전문 0420, MONTHLY_DISCOUNT 펀딩모델 활성화(94,376). ■ 비기능 (50→58) - docs/OPS_RUNBOOK.md: HA/DR·PCI·데이터보존·규제 설계 명시(코드불가 영역 정직 구분). ■ 화면·검증 - 시스템현황에 온보딩·KYC·3자대사 패널. - 통합빌드 error 0, 5,915 서비스, 3본 실호출 검증(상태기계 부적법전이 거부), 6단체인 COMMIT OK, 미결2PC 0. - 패리티 재채점 ≈87/100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d292772b1f
commit
bba433308a
14 changed files with 695 additions and 173 deletions
|
|
@ -152,6 +152,10 @@
|
|||
#include "MM_CHK_ORPHAN.h"
|
||||
#include "MM_CHK_GRADE.h"
|
||||
|
||||
/* ----- 가맹점 생애주기 (Wave K): 심사 상태기계 / KYC·AML 점검 */
|
||||
#include "MM_ONBOARD.h"
|
||||
#include "MM_KYC_CHECK.h"
|
||||
|
||||
/* ----- advertise table: {service name, function} (fn is extern from its file) */
|
||||
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
|
||||
{"MM_MERCH_REG", MM_MERCH_REG},
|
||||
|
|
@ -287,6 +291,8 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
|
|||
{"MM_CHK_MCC", MM_CHK_MCC},
|
||||
{"MM_CHK_ORPHAN", MM_CHK_ORPHAN},
|
||||
{"MM_CHK_GRADE", MM_CHK_GRADE},
|
||||
{"MM_ONBOARD", MM_ONBOARD},
|
||||
{"MM_KYC_CHECK", MM_KYC_CHECK},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
15
app/src/mm/svc/MM_KYC_CHECK.h
Normal file
15
app/src/mm/svc/MM_KYC_CHECK.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* MM_KYC_CHECK.h - mm 서비스 MM_KYC_CHECK 카피북 (copybook / record header).
|
||||
* KYC/AML 점검 수행·판정 후 mm_kyc 적재.
|
||||
* 사용 UBF 필드:
|
||||
* 입력 T_ID1(app_id), T_STR1(check_type: IDENTITY/SANCTION/PEP/AML/CREDIT)
|
||||
* 출력 T_STATUS(result: PASS/FAIL/REVIEW), T_ARG1(score), T_ID1(kyc_id), T_RC
|
||||
*/
|
||||
#ifndef MM_KYC_CHECK_H
|
||||
#define MM_KYC_CHECK_H
|
||||
|
||||
#include "mm_svc.h"
|
||||
|
||||
void MM_KYC_CHECK(TPSVCINFO *p);
|
||||
|
||||
#endif /* MM_KYC_CHECK_H */
|
||||
130
app/src/mm/svc/MM_KYC_CHECK.pgc
Normal file
130
app/src/mm/svc/MM_KYC_CHECK.pgc
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* MM_KYC_CHECK.pgc - mm 모듈 서비스 MM_KYC_CHECK (KYC/AML 점검 수행·판정).
|
||||
* 업무: mm_application.risk_score 를 근거로 check_type 별 판정을 수행하고
|
||||
* mm_kyc 에 점검 결과 1건을 적재한다.
|
||||
* 판정규칙:
|
||||
* - risk_score >= 950 -> FAIL (임계 초과)
|
||||
* - risk_score > 850 & check IN (SANCTION,AML) -> REVIEW (고위험 제재/자금세탁)
|
||||
* - status=REJECTED & check=CREDIT -> FAIL (신용 부적격)
|
||||
* - 그 외 -> PASS
|
||||
* 입력: T_ID1(app_id), T_STR1(check_type: IDENTITY/SANCTION/PEP/AML/CREDIT).
|
||||
* 출력: T_STATUS=result, T_ARG1=score, T_ID1=kyc_id, T_RC.
|
||||
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). mm_svr 가 tpsvrinit 에서 광고.
|
||||
*/
|
||||
#include "MM_KYC_CHECK.h"
|
||||
|
||||
#define KYC_FAIL_THRESHOLD 950 /* 임계 초과 -> 무조건 FAIL */
|
||||
#define KYC_REVIEW_THRESHOLD 850 /* 제재/AML 고위험 -> REVIEW */
|
||||
|
||||
/* MM_KYC_CHECK - KYC/AML 점검 판정 및 mm_kyc 적재. */
|
||||
void MM_KYC_CHECK(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
char check[24];
|
||||
int valid_type = 0;
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_app_id, h_risk, h_score, h_kyc_id, h_logid;
|
||||
char h_check[24], h_mid[64], h_astatus[16], h_result[16];
|
||||
char h_detail[160], h_svc[24], h_msg[200];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
|
||||
h_app_id = h_risk = h_score = h_kyc_id = h_logid = 0;
|
||||
check[0] = 0;
|
||||
h_check[0] = h_mid[0] = h_astatus[0] = 0;
|
||||
strcpy(h_result, "PASS");
|
||||
strcpy(h_svc, "MM_KYC_CHECK");
|
||||
userlog("[MM_KYC_CHECK] KYC 점검 진입");
|
||||
|
||||
/* [1] 입력 검증부 -------------------------------------------------- */
|
||||
h_app_id = getl(b, T_ID1);
|
||||
if (h_app_id <= 0) {
|
||||
setl(b, T_RC, 7101);
|
||||
Bchg(b, T_MSG, 0, "신청 ID(app_id) 필수", 0L);
|
||||
userlog("[MM_KYC_CHECK] app_id 검증 실패 id=%ld", h_app_id);
|
||||
FAIL(b);
|
||||
}
|
||||
gets_(b, T_STR1, check, sizeof(check));
|
||||
if (strcmp(check, "IDENTITY") == 0 || strcmp(check, "SANCTION") == 0 ||
|
||||
strcmp(check, "PEP") == 0 || strcmp(check, "AML") == 0 ||
|
||||
strcmp(check, "CREDIT") == 0)
|
||||
valid_type = 1;
|
||||
if (!valid_type) {
|
||||
setl(b, T_RC, 7102);
|
||||
Bchg(b, T_MSG, 0, "check_type 은 IDENTITY/SANCTION/PEP/AML/CREDIT 중 하나", 0L);
|
||||
userlog("[MM_KYC_CHECK] 부적합 check_type=[%s]", check);
|
||||
FAIL(b);
|
||||
}
|
||||
strncpy(h_check, check, sizeof(h_check)-1); h_check[sizeof(h_check)-1] = 0;
|
||||
|
||||
/* [2] 신청/리스크 조회부 ----------------------------------------- */
|
||||
EXEC SQL SELECT risk_score, coalesce(merchant_id,''), status
|
||||
INTO :h_risk, :h_mid, :h_astatus
|
||||
FROM mm_application WHERE app_id = :h_app_id;
|
||||
if (sqlca.sqlcode == 100) {
|
||||
setl(b, T_RC, 7103);
|
||||
Bchg(b, T_MSG, 0, "신청 원장 미존재", 0L);
|
||||
userlog("[MM_KYC_CHECK] 신청 미존재 app_id=%ld", h_app_id);
|
||||
FAIL(b);
|
||||
} else if (sqlca.sqlcode < 0) {
|
||||
userlog("[MM_KYC_CHECK] 신청 조회 오류 [%d]", sqlca.sqlcode);
|
||||
FAIL(b);
|
||||
}
|
||||
h_score = h_risk;
|
||||
userlog("[MM_KYC_CHECK] app_id=%ld risk=%ld status=%s check=%s",
|
||||
h_app_id, h_risk, h_astatus, h_check);
|
||||
|
||||
/* [3] 판정부 (임계/제재/신용) ------------------------------------- */
|
||||
if (h_risk >= KYC_FAIL_THRESHOLD) {
|
||||
strcpy(h_result, "FAIL");
|
||||
userlog("[MM_KYC_CHECK] 리스크 임계 초과 risk=%ld -> FAIL", h_risk);
|
||||
} else if (h_risk > KYC_REVIEW_THRESHOLD &&
|
||||
(strcmp(h_check, "SANCTION") == 0 || strcmp(h_check, "AML") == 0)) {
|
||||
strcpy(h_result, "REVIEW");
|
||||
userlog("[MM_KYC_CHECK] 고위험 제재/AML -> REVIEW");
|
||||
} else if (strcmp(h_astatus, "REJECTED") == 0 && strcmp(h_check, "CREDIT") == 0) {
|
||||
strcpy(h_result, "FAIL");
|
||||
userlog("[MM_KYC_CHECK] 반려 신청 신용점검 -> FAIL");
|
||||
} else {
|
||||
strcpy(h_result, "PASS");
|
||||
}
|
||||
|
||||
/* [4] 본처리부: mm_kyc 적재 (kyc_id = nextval mm_kyc_seq) ---------- */
|
||||
EXEC SQL SELECT nextval('mm_kyc_seq') INTO :h_kyc_id;
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_KYC_CHECK] kyc 시퀀스 실패"); FAIL(b); }
|
||||
snprintf(h_detail, sizeof(h_detail), "%s 자동점검 risk=%ld result=%s",
|
||||
h_check, h_risk, h_result);
|
||||
EXEC SQL INSERT INTO mm_kyc
|
||||
(kyc_id, app_id, merchant_id, check_type, result, score, detail)
|
||||
VALUES (:h_kyc_id, :h_app_id, :h_mid, :h_check, :h_result, :h_score, :h_detail);
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("[MM_KYC_CHECK] mm_kyc 적재 실패 [%d]", sqlca.sqlcode);
|
||||
setl(b, T_RC, 7104);
|
||||
Bchg(b, T_MSG, 0, "KYC 점검 결과 적재 실패", 0L);
|
||||
FAIL(b);
|
||||
}
|
||||
|
||||
/* [5] 이력/감사 적재부 (cm_log) ----------------------------------- */
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_KYC_CHECK] 로그 시퀀스 실패"); FAIL(b); }
|
||||
snprintf(h_msg, sizeof(h_msg), "MM_KYC_CHECK app=%ld type=%s result=%s score=%ld kyc=%ld",
|
||||
h_app_id, h_check, h_result, h_score, h_kyc_id);
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (:h_logid, :h_svc,
|
||||
CASE WHEN :h_result = 'PASS' THEN 'I' ELSE 'W' END,
|
||||
:h_msg, current_date);
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_KYC_CHECK] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
|
||||
/* [6] 응답부 (출력 필드 설정) ------------------------------------- */
|
||||
Bchg(b, T_STATUS, 0, h_result, 0L);
|
||||
setl(b, T_ARG1, h_score);
|
||||
setl(b, T_ID1, h_kyc_id);
|
||||
setl(b, T_RC, 0);
|
||||
|
||||
/* [7] 완료 로깅 --------------------------------------------------- */
|
||||
userlog("[MM_KYC_CHECK] 점검 완료 app_id=%ld type=%s result=%s kyc_id=%ld",
|
||||
h_app_id, h_check, h_result, h_kyc_id);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
16
app/src/mm/svc/MM_ONBOARD.h
Normal file
16
app/src/mm/svc/MM_ONBOARD.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* MM_ONBOARD.h - mm 서비스 MM_ONBOARD 카피북 (copybook / record header).
|
||||
* 가맹점 심사 진행(상태기계). RECEIVED->UNDERWRITING->APPROVED/REJECTED.
|
||||
* 사용 UBF 필드:
|
||||
* 입력 T_ID1(app_id), T_STR1(action: UNDERWRITE/APPROVE/REJECT),
|
||||
* T_STR3(reviewer, 선택), T_REASON(reject 사유, 선택)
|
||||
* 출력 T_STATUS(새 status), T_STR2(발급 merchant_id), T_RC
|
||||
*/
|
||||
#ifndef MM_ONBOARD_H
|
||||
#define MM_ONBOARD_H
|
||||
|
||||
#include "mm_svc.h"
|
||||
|
||||
void MM_ONBOARD(TPSVCINFO *p);
|
||||
|
||||
#endif /* MM_ONBOARD_H */
|
||||
140
app/src/mm/svc/MM_ONBOARD.pgc
Normal file
140
app/src/mm/svc/MM_ONBOARD.pgc
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* MM_ONBOARD.pgc - mm 모듈 서비스 MM_ONBOARD (가맹점 심사 진행 / 상태기계).
|
||||
* 업무: mm_application 의 심사 상태를 전이한다. 상태기계는 아래 전이만 허용하고
|
||||
* 그 외 전이는 부적법(不適法)으로 거부한다.
|
||||
* RECEIVED --UNDERWRITE--> UNDERWRITING
|
||||
* UNDERWRITING --APPROVE----> APPROVED (merchant_id 발급/decision_at/reviewer)
|
||||
* UNDERWRITING --REJECT-----> REJECTED (reject_reason/decision_at/reviewer)
|
||||
* APPROVED/REJECTED 는 종단 상태로 모든 전이를 거부한다.
|
||||
* 입력: T_ID1(app_id), T_STR1(action), T_STR3(reviewer 선택), T_REASON(반려사유 선택).
|
||||
* 출력: T_STATUS=새 status, T_STR2=merchant_id, T_RC.
|
||||
* 부적법 전이는 T_RC/T_MSG 세팅 후 FAIL(글로벌 트랜잭션 롤백).
|
||||
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). mm_svr 가 tpsvrinit 에서 광고.
|
||||
*/
|
||||
#include "MM_ONBOARD.h"
|
||||
|
||||
/* MM_ONBOARD - 가맹점 심사 상태기계 전이. */
|
||||
void MM_ONBOARD(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
char action[24], reviewer[64], reason[160];
|
||||
int legal = 0;
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_app_id, h_logid;
|
||||
char h_action[24], h_cur[16], h_new[16], h_mid[32];
|
||||
char h_reviewer[64], h_reason[160], h_svc[24], h_msg[200];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
|
||||
h_app_id = h_logid = 0;
|
||||
action[0] = reviewer[0] = reason[0] = 0;
|
||||
h_action[0] = h_cur[0] = h_new[0] = h_mid[0] = 0;
|
||||
h_reviewer[0] = h_reason[0] = 0;
|
||||
strcpy(h_svc, "MM_ONBOARD");
|
||||
userlog("[MM_ONBOARD] 심사 진행 진입");
|
||||
|
||||
/* [1] 입력 검증부 -------------------------------------------------- */
|
||||
h_app_id = getl(b, T_ID1);
|
||||
if (h_app_id <= 0) {
|
||||
setl(b, T_RC, 7001);
|
||||
Bchg(b, T_MSG, 0, "신청 ID(app_id) 필수", 0L);
|
||||
userlog("[MM_ONBOARD] app_id 검증 실패 id=%ld", h_app_id);
|
||||
FAIL(b);
|
||||
}
|
||||
gets_(b, T_STR1, action, sizeof(action));
|
||||
if (strcmp(action, "UNDERWRITE") != 0 &&
|
||||
strcmp(action, "APPROVE") != 0 &&
|
||||
strcmp(action, "REJECT") != 0) {
|
||||
setl(b, T_RC, 7002);
|
||||
Bchg(b, T_MSG, 0, "action 은 UNDERWRITE/APPROVE/REJECT 중 하나여야 함", 0L);
|
||||
userlog("[MM_ONBOARD] 부적합 action=[%s]", action);
|
||||
FAIL(b);
|
||||
}
|
||||
strncpy(h_action, action, sizeof(h_action)-1); h_action[sizeof(h_action)-1] = 0;
|
||||
|
||||
/* reviewer / reject 사유(선택 입력) */
|
||||
gets_(b, T_STR3, reviewer, sizeof(reviewer));
|
||||
if (reviewer[0] == 0) strcpy(reviewer, "AUTO");
|
||||
strncpy(h_reviewer, reviewer, sizeof(h_reviewer)-1); h_reviewer[sizeof(h_reviewer)-1] = 0;
|
||||
gets_(b, T_REASON, reason, sizeof(reason));
|
||||
|
||||
/* [2] 현재 상태 조회부 -------------------------------------------- */
|
||||
EXEC SQL SELECT status, coalesce(merchant_id,'')
|
||||
INTO :h_cur, :h_mid
|
||||
FROM mm_application WHERE app_id = :h_app_id;
|
||||
if (sqlca.sqlcode == 100) {
|
||||
setl(b, T_RC, 7003);
|
||||
Bchg(b, T_MSG, 0, "신청 원장 미존재", 0L);
|
||||
userlog("[MM_ONBOARD] 신청 미존재 app_id=%ld", h_app_id);
|
||||
FAIL(b);
|
||||
} else if (sqlca.sqlcode < 0) {
|
||||
userlog("[MM_ONBOARD] 신청 조회 오류 [%d]", sqlca.sqlcode);
|
||||
FAIL(b);
|
||||
}
|
||||
userlog("[MM_ONBOARD] app_id=%ld 현재상태=%s action=%s", h_app_id, h_cur, h_action);
|
||||
|
||||
/* [3] 상태기계 전이 판정부 (부적법 전이 거부) --------------------- */
|
||||
if (strcmp(h_action, "UNDERWRITE") == 0) {
|
||||
if (strcmp(h_cur, "RECEIVED") == 0) { strcpy(h_new, "UNDERWRITING"); legal = 1; }
|
||||
} else if (strcmp(h_action, "APPROVE") == 0) {
|
||||
if (strcmp(h_cur, "UNDERWRITING") == 0) { strcpy(h_new, "APPROVED"); legal = 1; }
|
||||
} else if (strcmp(h_action, "REJECT") == 0) {
|
||||
if (strcmp(h_cur, "UNDERWRITING") == 0) { strcpy(h_new, "REJECTED"); legal = 1; }
|
||||
}
|
||||
if (!legal) {
|
||||
setl(b, T_RC, 7010);
|
||||
snprintf(h_msg, sizeof(h_msg),
|
||||
"부적법 전이 거부: %s 상태에서 %s 불가", h_cur, h_action);
|
||||
Bchg(b, T_MSG, 0, h_msg, 0L);
|
||||
userlog("[MM_ONBOARD] %s", h_msg);
|
||||
FAIL(b);
|
||||
}
|
||||
|
||||
/* [4] 본처리부: 상태별 UPDATE ------------------------------------- */
|
||||
if (strcmp(h_new, "UNDERWRITING") == 0) {
|
||||
EXEC SQL UPDATE mm_application SET status = 'UNDERWRITING'
|
||||
WHERE app_id = :h_app_id;
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_ONBOARD] UNDERWRITING 갱신 실패"); FAIL(b); }
|
||||
h_mid[0] = 0; /* 아직 MID 미발급 */
|
||||
} else if (strcmp(h_new, "APPROVED") == 0) {
|
||||
/* merchant_id 발급: 'M' + app_id zero-padding (호스트변수 캐스팅 회피, C에서 조립) */
|
||||
snprintf(h_mid, sizeof(h_mid), "M%09ld", h_app_id);
|
||||
EXEC SQL UPDATE mm_application
|
||||
SET status = 'APPROVED', merchant_id = :h_mid,
|
||||
reviewer = :h_reviewer, decision_at = now()
|
||||
WHERE app_id = :h_app_id;
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_ONBOARD] APPROVED 갱신 실패"); FAIL(b); }
|
||||
userlog("[MM_ONBOARD] 승인 app_id=%ld mid=[%s] reviewer=[%s]",
|
||||
h_app_id, h_mid, h_reviewer);
|
||||
} else { /* REJECTED */
|
||||
if (reason[0] == 0) strcpy(reason, "심사 반려");
|
||||
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
|
||||
EXEC SQL UPDATE mm_application
|
||||
SET status = 'REJECTED', reviewer = :h_reviewer,
|
||||
reject_reason = :h_reason, decision_at = now()
|
||||
WHERE app_id = :h_app_id;
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_ONBOARD] REJECTED 갱신 실패"); FAIL(b); }
|
||||
h_mid[0] = 0;
|
||||
userlog("[MM_ONBOARD] 반려 app_id=%ld reason=[%s]", h_app_id, h_reason);
|
||||
}
|
||||
|
||||
/* [5] 이력/감사 적재부 (cm_log) ----------------------------------- */
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_ONBOARD] 로그 시퀀스 실패"); FAIL(b); }
|
||||
snprintf(h_msg, sizeof(h_msg), "MM_ONBOARD app=%ld %s: %s -> %s mid=%s",
|
||||
h_app_id, h_action, h_cur, h_new, h_mid[0] ? h_mid : "-");
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (:h_logid, :h_svc, 'I', :h_msg, current_date);
|
||||
if (sqlca.sqlcode < 0) { userlog("[MM_ONBOARD] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
|
||||
/* [6] 응답부 (출력 필드 설정) ------------------------------------- */
|
||||
Bchg(b, T_STATUS, 0, h_new, 0L);
|
||||
Bchg(b, T_STR2, 0, h_mid, 0L); /* 승인시 발급 MID, 그 외 빈값 */
|
||||
setl(b, T_RC, 0);
|
||||
|
||||
/* [7] 완료 로깅 --------------------------------------------------- */
|
||||
userlog("[MM_ONBOARD] 전이 완료 app_id=%ld %s -> %s", h_app_id, h_cur, h_new);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
|
|
@ -1,21 +1,15 @@
|
|||
/*
|
||||
* RC_3WAY.h - rc 서비스 RC_3WAY 카피북 (copybook / record header).
|
||||
* 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED.
|
||||
* 3자 대사(카드망 정산파일 ↔ 내부원장 ↔ 은행입금): rc_3way UPSERT.
|
||||
* 사용 UBF 필드:
|
||||
* 입력 T_BIZDATE(영업일), T_KEY1(member_bin)
|
||||
* 출력 T_AMT1(amt_network), T_ARG1(diff_ni), T_STATUS(status), T_RC
|
||||
*/
|
||||
#ifndef RC_3WAY_H
|
||||
#define RC_3WAY_H
|
||||
|
||||
#include "rc_svc.h"
|
||||
|
||||
typedef struct {
|
||||
long purchase_id;
|
||||
char merchant_id[64];
|
||||
long amount;
|
||||
long net;
|
||||
char status[8];
|
||||
char biz_date[16];
|
||||
} rc_rc_3way_rec_t;
|
||||
|
||||
void RC_3WAY(TPSVCINFO *p);
|
||||
|
||||
#endif /* RC_3WAY_H */
|
||||
|
|
|
|||
|
|
@ -1,192 +1,147 @@
|
|||
/*
|
||||
* RC_3WAY.pgc - rc 모듈 서비스 RC_3WAY (대사 온라인 프로그램, 심화판).
|
||||
* 업무: 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED.
|
||||
* 처리단계: 입력검증->마스터/정책조회->업무규칙분기->본처리->이력적재->집계갱신->예외보상->응답.
|
||||
* RC_3WAY.pgc - rc 모듈 서비스 RC_3WAY (3자 대사 온라인 프로그램).
|
||||
* 업무: 카드망 정산파일(st_net_settlement) ↔ 내부원장(settlement/ledger) ↔
|
||||
* 은행실입금(st_funding_instr) 3자 대사. 차이 산출 후 rc_3way UPSERT.
|
||||
* 입력: T_BIZDATE(영업일), T_KEY1(member_bin).
|
||||
* 출력: T_AMT1=amt_network, T_ARG1=diff_ni, T_STATUS=status, T_RC.
|
||||
* 처리단계: 입력검증->망/내부/은행 3자 조회->차이산출->상태판정->rc_3way UPSERT
|
||||
* ->감사적재->응답.
|
||||
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). rc_svr 가 tpsvrinit 에서 광고.
|
||||
*/
|
||||
#include "RC_3WAY.h"
|
||||
#include "acq_common.h"
|
||||
|
||||
/* RC_3WAY - 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. */
|
||||
/* RC_3WAY - 3자 대사(망·내부·은행) 차이 산출 및 rc_3way UPSERT. */
|
||||
void RC_3WAY(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
char bizdate[16], tmpin[64];
|
||||
char sbuf[300];
|
||||
long tol = 0, rc_code = 0, adiff = 0, out_cnt = 0, pctl = 0;
|
||||
long scan_rows = 0, matched_rows = 0, unmatched_rows = 0, unmatched_amt = 0;
|
||||
char bizin[16], binin[64];
|
||||
long diff_ni = 0, diff_nb = 0; /* 로컬 산출(호스트변수 아님) */
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid, h_amt, h_fee, h_net, h_act, h_exp, h_gross, h_cnt;
|
||||
long h_mcnt, h_ecnt, h_ucnt, h_led, h_set, h_id, h_id2, h_mdr;
|
||||
long h_limit, h_sum, h_val, h_n1, h_n2, h_prev, h_diff;
|
||||
char h_bizdate[16], h_nbizdate[16], h_merch[64], h_status[16], h_pstat[16];
|
||||
char h_channel[16], h_issuer[40], h_category[16], h_ccy[8], h_reason[160];
|
||||
char h_msg[256], h_svc[40], h_lvl[16], h_key[64], h_dtype[8];
|
||||
char h_bizdate[16], h_bin[64], h_status[16], h_svc[24], h_reason[192];
|
||||
long h_net_amt, h_internal, h_bank, h_diff_ni, h_diff_nb;
|
||||
long h_ncnt, h_icnt, h_bcnt, h_logid;
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
|
||||
h_pid = h_amt = h_fee = h_net = h_act = h_exp = h_gross = h_cnt = 0;
|
||||
h_mcnt = h_ecnt = h_ucnt = h_led = h_set = h_id = h_id2 = h_mdr = 0;
|
||||
h_limit = h_sum = h_val = h_n1 = h_n2 = h_prev = h_diff = 0;
|
||||
bizdate[0] = tmpin[0] = 0;
|
||||
h_bizdate[0] = h_nbizdate[0] = h_merch[0] = 0;
|
||||
strcpy(h_status, "MATCHED"); strcpy(h_pstat, "A"); strcpy(h_ccy, "KRW");
|
||||
strcpy(h_channel, "POS"); strcpy(h_issuer, "UNKNOWN"); strcpy(h_category, "GEN");
|
||||
strcpy(h_reason, "3WAY_CHK"); strcpy(h_lvl, "I"); strcpy(h_dtype, "AMT");
|
||||
h_net_amt = h_internal = h_bank = h_diff_ni = h_diff_nb = 0;
|
||||
h_ncnt = h_icnt = h_bcnt = h_logid = 0;
|
||||
bizin[0] = binin[0] = 0;
|
||||
h_bizdate[0] = h_bin[0] = 0;
|
||||
strcpy(h_status, "PENDING");
|
||||
strcpy(h_svc, "RC_3WAY");
|
||||
snprintf(h_msg, sizeof(h_msg), "[RC_3WAY] 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED.");
|
||||
strcpy(h_key, "-");
|
||||
userlog("[RC_3WAY] 대사 진입: 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED.");
|
||||
strcpy(h_reason, "3WAY_RECON");
|
||||
userlog("[RC_3WAY] 3자 대사 진입");
|
||||
|
||||
/* [1] 입력 검증부 -------------------------------------------------- */
|
||||
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
|
||||
if (strlen(bizdate) < 8) {
|
||||
setl(b, T_RC, 8001); Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L);
|
||||
userlog("[RC_3WAY] 영업일자 검증 실패 val=[%s]", bizdate); FAIL(b);
|
||||
gets_(b, T_BIZDATE, bizin, sizeof(bizin));
|
||||
if (strlen(bizin) < 8) {
|
||||
setl(b, T_RC, 8001);
|
||||
Bchg(b, T_MSG, 0, "영업일자(biz_date) 형식 오류", 0L);
|
||||
userlog("[RC_3WAY] 영업일 검증 실패 val=[%s]", bizin);
|
||||
FAIL(b);
|
||||
}
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
strcpy(h_key, bizdate);
|
||||
h_pid = getl(b, T_PURCHASE_ID);
|
||||
if (h_pid <= 0) {
|
||||
setl(b, T_RC, 8002); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
|
||||
userlog("[RC_3WAY] 매입 ID 검증 실패 pid=%ld", h_pid); FAIL(b);
|
||||
strncpy(h_bizdate, bizin, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
gets_(b, T_KEY1, binin, sizeof(binin));
|
||||
if (binin[0] == 0) {
|
||||
setl(b, T_RC, 8002);
|
||||
Bchg(b, T_MSG, 0, "회원사 BIN(member_bin) 필수", 0L);
|
||||
userlog("[RC_3WAY] member_bin 미지정");
|
||||
FAIL(b);
|
||||
}
|
||||
h_net = getl(b, T_NET);
|
||||
if (h_net < -100000000000L || h_net > 100000000000L) {
|
||||
setl(b, T_RC, 8003); Bchg(b, T_MSG, 0, "순액 범위 초과", 0L);
|
||||
userlog("[RC_3WAY] 순액 범위 초과 net=%ld", h_net); FAIL(b);
|
||||
strncpy(h_bin, binin, sizeof(h_bin)-1); h_bin[sizeof(h_bin)-1] = 0;
|
||||
userlog("[RC_3WAY] 대사 대상 date=[%s] bin=[%s]", h_bizdate, h_bin);
|
||||
|
||||
/* [2-1] 카드망(st_net_settlement) 순액 조회 — 망 레그 --------------- */
|
||||
EXEC SQL SELECT coalesce(sum(net_amt),0), count(*)
|
||||
INTO :h_net_amt, :h_ncnt
|
||||
FROM st_net_settlement
|
||||
WHERE member_bin = :h_bin AND biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 망 정산 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
if (h_ncnt == 0)
|
||||
userlog("[RC_3WAY] 망 정산 데이터 없음 bin=[%s] date=[%s]", h_bin, h_bizdate);
|
||||
|
||||
/* [2-2] 내부 원장(settlement 합, 없으면 ledger SETTLE 합) — 내부 레그
|
||||
* 내부원장은 member_bin 키가 없으므로 당일 순액 합으로 근사한다. */
|
||||
EXEC SQL SELECT coalesce(sum(net),0), count(*)
|
||||
INTO :h_internal, :h_icnt
|
||||
FROM settlement WHERE biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 정산원장 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
if (h_icnt == 0) {
|
||||
EXEC SQL SELECT coalesce(sum(amount),0)
|
||||
INTO :h_internal
|
||||
FROM ledger WHERE biz_date = :h_bizdate AND entry_type IN ('SETTLE','RECON');
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 원장 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
userlog("[RC_3WAY] 내부 레그 ledger 근사 적용 internal=%ld", h_internal);
|
||||
}
|
||||
|
||||
/* [2] 마스터/정책/상태 조회부 ------------------------------------- */
|
||||
EXEC SQL SELECT amount, fee, net, status, merchant_id, channel, issuer, category, biz_date
|
||||
INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_channel, :h_issuer, :h_category, :h_bizdate
|
||||
FROM purchase WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode == 100) {
|
||||
setl(b, T_RC, 8004); Bchg(b, T_MSG, 0, "매입 원장 미존재", 0L);
|
||||
userlog("[RC_3WAY] 매입 원장 미존재 pid=%ld", h_pid); FAIL(b);
|
||||
} else if (sqlca.sqlcode < 0) {
|
||||
userlog("[RC_3WAY] 매입 조회 오류 [%d]", sqlca.sqlcode); FAIL(b);
|
||||
}
|
||||
strncpy(bizdate, h_bizdate, sizeof(bizdate)-1); bizdate[sizeof(bizdate)-1] = 0;
|
||||
EXEC SQL SELECT mdr_bps, daily_limit, status INTO :h_mdr, :h_limit, :h_status
|
||||
FROM merchant WHERE merchant_id = :h_merch;
|
||||
if (sqlca.sqlcode == 100) {
|
||||
strcpy(h_status, "NOMERCH"); h_mdr = 25; h_limit = 100000000;
|
||||
userlog("[RC_3WAY] 가맹점 마스터 미존재 mid=[%s] 기본정책 적용", h_merch);
|
||||
} else if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 가맹점 조회 오류"); FAIL(b); }
|
||||
EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(net),0), coalesce(sum(fee),0)
|
||||
INTO :h_cnt, :h_gross, :h_sum, :h_fee FROM purchase WHERE biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 당일 매입 집계 오류"); FAIL(b); }
|
||||
EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log
|
||||
WHERE biz_date = :h_bizdate AND status = 'MATCHED';
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 대사 로그 집계 오류"); FAIL(b); }
|
||||
EXEC SQL SELECT count(*) INTO :h_ecnt FROM rc_exception
|
||||
WHERE biz_date = :h_bizdate AND resolved = false;
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 예외 집계 오류"); FAIL(b); }
|
||||
/* [2-3] 은행 실입금(st_funding_instr 합) — 은행 레그(근사) ---------- */
|
||||
EXEC SQL SELECT coalesce(sum(amount),0), count(*)
|
||||
INTO :h_bank, :h_bcnt
|
||||
FROM st_funding_instr
|
||||
WHERE biz_date = :h_bizdate AND status IN ('SENT','CONFIRMED');
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 은행 입금 조회 오류 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
|
||||
/* [3] 거래집합 커서 순회 + 허용오차/매칭분류 --------------------- */
|
||||
EXEC SQL DECLARE cur_rc_3way CURSOR FOR
|
||||
SELECT purchase_id, net, amount, fee, status
|
||||
FROM purchase WHERE biz_date = :h_bizdate ORDER BY purchase_id;
|
||||
EXEC SQL OPEN cur_rc_3way;
|
||||
for (;;) {
|
||||
EXEC SQL FETCH cur_rc_3way INTO :h_pid, :h_net, :h_amt, :h_fee, :h_pstat;
|
||||
if (sqlca.sqlcode == 100) break;
|
||||
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_3way; userlog("[RC_3WAY] 커서 FETCH 오류"); FAIL(b); }
|
||||
EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger
|
||||
WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE');
|
||||
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cur_rc_3way; FAIL(b); }
|
||||
h_exp = h_net; adiff = (h_exp - h_act); if (adiff < 0) adiff = -adiff;
|
||||
scan_rows++;
|
||||
if (adiff <= (tol > 0 ? tol : 0)) matched_rows++;
|
||||
else { unmatched_rows++; unmatched_amt += adiff; }
|
||||
}
|
||||
EXEC SQL CLOSE cur_rc_3way;
|
||||
userlog("[RC_3WAY] 대사스캔 완료 scan=%ld match=%ld unmatch=%ld", scan_rows, matched_rows, unmatched_rows);
|
||||
/* [3] 차이 산출부 (diff_ni = 망-내부, diff_nb = 망-은행) ----------- */
|
||||
diff_ni = h_net_amt - h_internal;
|
||||
diff_nb = h_net_amt - h_bank;
|
||||
h_diff_ni = diff_ni;
|
||||
h_diff_nb = diff_nb;
|
||||
userlog("[RC_3WAY] 산출 net=%ld internal=%ld bank=%ld diff_ni=%ld diff_nb=%ld",
|
||||
h_net_amt, h_internal, h_bank, h_diff_ni, h_diff_nb);
|
||||
|
||||
/* [4] 업무규칙 분기부 (상태/금액/한도) ----------------------------- */
|
||||
if (h_gross <= 0 && scan_rows == 0) {
|
||||
rc_code = 0; strcpy(h_status, "NODATA"); out_cnt = 0;
|
||||
userlog("[RC_3WAY] 대상 없음 - 무처리 종료");
|
||||
} else if (unmatched_rows == 0) {
|
||||
rc_code = 0; strcpy(h_status, "MATCHED"); out_cnt = matched_rows;
|
||||
} else if (unmatched_rows * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) {
|
||||
rc_code = 1; strcpy(h_status, "TOLERATED"); out_cnt = unmatched_rows;
|
||||
userlog("[RC_3WAY] 경미 불일치 허용범위 내 unmatch=%ld", unmatched_rows);
|
||||
/* [4] 상태 판정부 (차이 0 -> MATCHED, 그 외 -> DIFF, 망無 -> PENDING) */
|
||||
if (h_ncnt == 0) {
|
||||
strcpy(h_status, "PENDING");
|
||||
userlog("[RC_3WAY] 망 데이터 부재 -> PENDING");
|
||||
} else if (h_diff_ni == 0 && h_diff_nb == 0) {
|
||||
strcpy(h_status, "MATCHED");
|
||||
} else {
|
||||
rc_code = 2; strcpy(h_status, "EXCEPTION"); out_cnt = unmatched_rows;
|
||||
userlog("[RC_3WAY] 대사 불일치 초과 unmatch=%ld amt=%ld", unmatched_rows, unmatched_amt);
|
||||
strcpy(h_status, "DIFF");
|
||||
userlog("[RC_3WAY] 3자 불일치 diff_ni=%ld diff_nb=%ld", h_diff_ni, h_diff_nb);
|
||||
}
|
||||
if (h_mdr > 0 && h_gross > 0) pctl = acq_fee(h_gross, h_mdr); /* 이론 수수료 */
|
||||
h_exp = h_net; h_diff = h_exp - h_act;
|
||||
|
||||
/* [5] 본처리부 (핵심 적재/갱신) ----------------------------------- */
|
||||
/* 대사 로그 적재 (rc_match_log) */
|
||||
h_id = rcdb_insert_match(h_pid, "3WAY", h_net,
|
||||
(unmatched_rows == 0 ? "MATCHED" : "EXCEPTION"), bizdate);
|
||||
if (h_id < 0) { userlog("[RC_3WAY] 대사 로그 적재 실패 pid=%ld", h_pid); FAIL(b); }
|
||||
out_cnt = matched_rows > 0 ? matched_rows : 1;
|
||||
/* [5] 본처리부: rc_3way UPSERT (biz_date, member_bin) --------------- */
|
||||
EXEC SQL INSERT INTO rc_3way
|
||||
(recon_id, biz_date, member_bin, amt_network, amt_internal, amt_bank,
|
||||
diff_ni, diff_nb, status)
|
||||
VALUES (nextval('rc_3way_seq'), :h_bizdate, :h_bin, :h_net_amt, :h_internal,
|
||||
:h_bank, :h_diff_ni, :h_diff_nb, :h_status)
|
||||
ON CONFLICT (biz_date, member_bin) DO UPDATE SET
|
||||
amt_network = EXCLUDED.amt_network,
|
||||
amt_internal = EXCLUDED.amt_internal,
|
||||
amt_bank = EXCLUDED.amt_bank,
|
||||
diff_ni = EXCLUDED.diff_ni,
|
||||
diff_nb = EXCLUDED.diff_nb,
|
||||
status = EXCLUDED.status;
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("[RC_3WAY] rc_3way UPSERT 실패 [%d]", sqlca.sqlcode);
|
||||
setl(b, T_RC, 8003);
|
||||
Bchg(b, T_MSG, 0, "3자대사 결과 적재 실패", 0L);
|
||||
FAIL(b);
|
||||
}
|
||||
|
||||
/* [6] 이력/감사 적재부 (cm_log) ----------------------------------- */
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id2;
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 로그 시퀀스 실패"); FAIL(b); }
|
||||
snprintf(sbuf, sizeof(sbuf), "RC_3WAY scan=%ld match=%ld unmatch=%ld gross=%ld rc=%ld",
|
||||
scan_rows, matched_rows, unmatched_rows, h_gross, rc_code);
|
||||
strncpy(h_reason, sbuf, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
|
||||
strcpy(h_lvl, rc_code >= 2 ? "E" : (rc_code == 1 ? "W" : "I"));
|
||||
snprintf(h_reason, sizeof(h_reason),
|
||||
"RC_3WAY bin=%s net=%ld internal=%ld bank=%ld dni=%ld dnb=%ld st=%s",
|
||||
h_bin, h_net_amt, h_internal, h_bank, h_diff_ni, h_diff_nb, h_status);
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (:h_id2, :h_svc, :h_lvl, :h_reason, :h_bizdate);
|
||||
VALUES (:h_logid, :h_svc,
|
||||
CASE WHEN :h_diff_ni = 0 AND :h_diff_nb = 0 THEN 'I' ELSE 'W' END,
|
||||
:h_reason, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 감사 로그 적재 실패 [%d]", sqlca.sqlcode); FAIL(b); }
|
||||
|
||||
/* [7] 집계/요약 재집계부 ------------------------------------------ */
|
||||
EXEC SQL SELECT count(*) INTO :h_ucnt FROM purchase p
|
||||
WHERE p.biz_date = :h_bizdate
|
||||
AND p.status IN ('A','H');
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 미결 재집계 오류"); FAIL(b); }
|
||||
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_set FROM settlement WHERE biz_date = :h_bizdate;
|
||||
if (sqlca.sqlcode < 0) { userlog("[RC_3WAY] 정산 재집계 오류"); FAIL(b); }
|
||||
if (h_val == 0) h_val = h_set;
|
||||
if (h_n1 == 0) h_n1 = matched_rows;
|
||||
if (h_n2 == 0) h_n2 = unmatched_rows;
|
||||
/* [7] 응답부 (출력 필드 설정) ------------------------------------- */
|
||||
setl(b, T_AMT1, h_net_amt); /* 카드망 순액 */
|
||||
setl(b, T_ARG1, h_diff_ni); /* 망-내부 차이 */
|
||||
setl(b, T_AMT2, h_diff_nb); /* 망-은행 차이(참조) */
|
||||
Bchg(b, T_STATUS, 0, h_status, 0L);
|
||||
setl(b, T_RC, 0);
|
||||
|
||||
/* [7-1] 후속 업무 체인 (tpcall) — CHAIN-BEGIN -----------------------
|
||||
* 하위 서비스는 별도 XA 브랜치라 이 브랜치의 미커밋 행이 보이지 않는다.
|
||||
* 판단에 필요한 값은 전문(T_*)에 실어 전달하고, 응답 계약은 [8] 응답부가
|
||||
* 뒤에서 재확정한다(하위 결과코드가 기존 계약을 덮지 않도록 복원). */
|
||||
{
|
||||
long ch_rlen = 0, ch_sub = 0, ch_keep = getl(b, T_RC);
|
||||
long ch_pid = getl(b, T_PURCHASE_ID);
|
||||
setl(b, T_AMOUNT, h_amt); setl(b, T_FEE, h_fee); setl(b, T_NET, h_net);
|
||||
setl(b, T_GROSS, h_gross); setl(b, T_COUNT, scan_rows);
|
||||
setl(b, T_ARG3, unmatched_rows); setl(b, T_AMT2, unmatched_amt);
|
||||
setl(b, T_ARG4, rc_code);
|
||||
Bchg(b, T_BIZDATE, 0, h_bizdate, 0L);
|
||||
if (h_merch[0]) Bchg(b, T_MERCHANT, 0, h_merch, 0L);
|
||||
Bchg(b, T_SVCNAME, 0, h_svc, 0L);
|
||||
userlog("[RC_3WAY] 승인-매입 정합성 검증 연계 -> VL_APPR_ACQ pid=%ld date=[%s]", ch_pid, h_bizdate);
|
||||
if (tpcall("VL_APPR_ACQ", (char *)b, 0L, (char **)&b, &ch_rlen, 0L) < 0) {
|
||||
userlog("[RC_3WAY] tpcall(VL_APPR_ACQ) 실패: %s", tpstrerror(tperrno));
|
||||
setl(b, T_RC, 8811); Bchg(b, T_MSG, 0, "승인-매입 정합성 검증 연계 실패", 0L);
|
||||
FAIL(b);
|
||||
}
|
||||
ch_sub = getl(b, T_RC);
|
||||
if (ch_sub != 0) userlog("[RC_3WAY] VL_APPR_ACQ 이상 회신 rc=%ld", ch_sub);
|
||||
userlog("[RC_3WAY] 원장 3-way 대조 연계 -> LG_THREEWAY pid=%ld date=[%s]", ch_pid, h_bizdate);
|
||||
if (tpcall("LG_THREEWAY", (char *)b, 0L, (char **)&b, &ch_rlen, 0L) < 0) {
|
||||
userlog("[RC_3WAY] tpcall(LG_THREEWAY) 실패: %s", tpstrerror(tperrno));
|
||||
setl(b, T_RC, 8812); Bchg(b, T_MSG, 0, "원장 3-way 대조 연계 실패", 0L);
|
||||
FAIL(b);
|
||||
}
|
||||
ch_sub = getl(b, T_RC);
|
||||
if (ch_sub != 0) userlog("[RC_3WAY] LG_THREEWAY 이상 회신 rc=%ld", ch_sub);
|
||||
setl(b, T_RC, ch_keep); /* 기존 응답 계약 복원 — [8]에서 최종 확정 */
|
||||
}
|
||||
|
||||
/* [8] 응답부 (출력 필드 설정) ------------------------------------- */
|
||||
setl(b, T_RC, rc_code);
|
||||
|
||||
/* [9] 완료 로깅 --------------------------------------------------- */
|
||||
userlog("[RC_3WAY] 대사 정상 종료 rc=%ld out=%ld status=%s", rc_code, out_cnt, h_status);
|
||||
/* [8] 완료 로깅 --------------------------------------------------- */
|
||||
userlog("[RC_3WAY] 3자 대사 종료 bin=[%s] status=%s diff_ni=%ld",
|
||||
h_bin, h_status, h_diff_ni);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
|||
/* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */
|
||||
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4652,"u":"개"},{"k":"총 코드 라인","v":332823,"u":"LOC"},{"k":"온라인 서비스","v":1476,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23871,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47285,"u":"곳"},{"k":"DB 테이블","v":95,"u":"개"},{"k":"카피북 헤더(.h)","v":2220,"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":23871,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47285,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6918,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2260,"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":2220,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2260,"goto":64,"userlog":17249,"sqlca_checks":18862,"embedded_sql_density":9.8},"ORACLE":{"asis_files":9,"asis_proc":7,"asis_plsql":2,"asis_loc":1827,"dialects":[{"t":"PL/SQL 패키지 호출 (pkg_x.proc)","n":18},{"t":"NVL / NVL2 널가드","n":60},{"t":"DECODE 다분기","n":16},{"t":"SYSDATE / SYSTIMESTAMP","n":42},{"t":"FROM DUAL","n":15},{"t":"시퀀스 .NEXTVAL","n":27},{"t":"ROWNUM 의사컬럼","n":6},{"t":"구식 외부조인 (+)","n":8},{"t":"CONNECT BY 계층질의","n":3},{"t":"WHENEVER SQLERROR 라벨분기","n":16},{"t":"TO_DATE / TO_CHAR 포맷","n":35},{"t":"RAISE_APPLICATION_ERROR","n":13}]}};
|
||||
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4656,"u":"개"},{"k":"총 코드 라인","v":333079,"u":"LOC"},{"k":"온라인 서비스","v":1478,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23879,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":985,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47295,"u":"곳"},{"k":"DB 테이블","v":99,"u":"개"},{"k":"카피북 헤더(.h)","v":2222,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":985,"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":23879,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47295,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6921,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2264,"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":2222,"copybook_shared_max":138,"copybook_shared_name":"acq_common.h","macro_defs":2264,"goto":64,"userlog":17267,"sqlca_checks":18877,"embedded_sql_density":9.8},"ORACLE":{"asis_files":9,"asis_proc":7,"asis_plsql":2,"asis_loc":1827,"dialects":[{"t":"PL/SQL 패키지 호출 (pkg_x.proc)","n":18},{"t":"NVL / NVL2 널가드","n":60},{"t":"DECODE 다분기","n":16},{"t":"SYSDATE / SYSTIMESTAMP","n":42},{"t":"FROM DUAL","n":15},{"t":"시퀀스 .NEXTVAL","n":27},{"t":"ROWNUM 의사컬럼","n":6},{"t":"구식 외부조인 (+)","n":8},{"t":"CONNECT BY 계층질의","n":3},{"t":"WHENEVER SQLERROR 라벨분기","n":16},{"t":"TO_DATE / TO_CHAR 포맷","n":35},{"t":"RAISE_APPLICATION_ERROR","n":13}]}};
|
||||
|
|
|
|||
77
db/schema.d/73-lifecycle.sql
Normal file
77
db/schema.d/73-lifecycle.sql
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
-- ===========================================================================
|
||||
-- 73-lifecycle.sql : 잔여 패리티 갭 (Wave K) — 온보딩·KYC·3자대사·ISO전문·계정
|
||||
-- 가맹점 심사/언더라이팅, KYC/AML, 3자대사(망·내부·은행), 계정체계(suspense/
|
||||
-- clearing), 프레젠트먼트 윈도우. 기존 구조 불변(추가만).
|
||||
-- ===========================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- K-1. 가맹점 온보딩 / 언더라이팅 (심사)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS mm_app_seq START 300000;
|
||||
CREATE TABLE IF NOT EXISTS mm_application (
|
||||
app_id BIGINT PRIMARY KEY,
|
||||
biz_no TEXT NOT NULL, -- 사업자등록번호
|
||||
name TEXT NOT NULL,
|
||||
mcc TEXT NOT NULL,
|
||||
rep_name TEXT NOT NULL DEFAULT '', -- 대표자
|
||||
est_monthly BIGINT NOT NULL DEFAULT 0, -- 예상 월매출
|
||||
risk_score INTEGER NOT NULL DEFAULT 0, -- 언더라이팅 리스크(0~999)
|
||||
status TEXT NOT NULL DEFAULT 'RECEIVED', -- RECEIVED/UNDERWRITING/APPROVED/REJECTED
|
||||
reviewer TEXT,
|
||||
decision_at TIMESTAMPTZ,
|
||||
merchant_id TEXT, -- 승인시 발급된 MID
|
||||
reject_reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_app_status ON mm_application(status);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- K-2. KYC / AML 점검
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS mm_kyc_seq START 320000;
|
||||
CREATE TABLE IF NOT EXISTS mm_kyc (
|
||||
kyc_id BIGINT PRIMARY KEY,
|
||||
app_id BIGINT,
|
||||
merchant_id TEXT,
|
||||
check_type TEXT NOT NULL, -- IDENTITY/SANCTION/PEP/AML/CREDIT
|
||||
result TEXT NOT NULL DEFAULT 'PASS',-- PASS/FAIL/REVIEW
|
||||
score INTEGER NOT NULL DEFAULT 0,
|
||||
detail TEXT,
|
||||
checked_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_kyc_app ON mm_kyc(app_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- K-3. 3자 대사 (카드망 정산파일 ↔ 내부원장 ↔ 은행입금)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE SEQUENCE IF NOT EXISTS rc_3way_seq START 340000;
|
||||
CREATE TABLE IF NOT EXISTS rc_3way (
|
||||
recon_id BIGINT PRIMARY KEY,
|
||||
biz_date DATE NOT NULL,
|
||||
member_bin TEXT NOT NULL,
|
||||
amt_network BIGINT NOT NULL DEFAULT 0, -- 카드망 정산파일 금액
|
||||
amt_internal BIGINT NOT NULL DEFAULT 0, -- 내부 원장 금액
|
||||
amt_bank BIGINT NOT NULL DEFAULT 0, -- 은행 실입금 금액
|
||||
diff_ni BIGINT NOT NULL DEFAULT 0, -- 망-내부 차이
|
||||
diff_nb BIGINT NOT NULL DEFAULT 0, -- 망-은행 차이
|
||||
status TEXT NOT NULL DEFAULT 'MATCHED', -- MATCHED/DIFF/PENDING
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (biz_date, member_bin)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- K-4. 총계정 계정과목 (suspense/clearing 분리) — 원장 계정체계
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS lg_account (
|
||||
acct_code TEXT PRIMARY KEY, -- 계정코드
|
||||
acct_name TEXT NOT NULL,
|
||||
acct_type TEXT NOT NULL, -- ASSET/LIABILITY/REVENUE/EXPENSE/SUSPENSE/CLEARING
|
||||
normal_side TEXT NOT NULL DEFAULT 'D' -- 정상잔액 방향 D/C
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- K-5. 프레젠트먼트 윈도우 / interchange downgrade 사유 (기존 테이블 확장)
|
||||
-- ---------------------------------------------------------------------------
|
||||
ALTER TABLE cl_presentment ADD COLUMN IF NOT EXISTS window_days INTEGER NOT NULL DEFAULT 1;
|
||||
ALTER TABLE cl_presentment ADD COLUMN IF NOT EXISTS late_flag BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE st_fee_component ADD COLUMN IF NOT EXISTS downgrade TEXT; -- NULL=정상 등급, 값=강등사유
|
||||
97
db/schema.d/97d-lifecycle-seed.sql
Normal file
97
db/schema.d/97d-lifecycle-seed.sql
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
-- ===========================================================================
|
||||
-- 97d-lifecycle-seed.sql : Wave K 시드 (73-lifecycle DDL 이후).
|
||||
-- 온보딩·KYC·3자대사·계정과목·프레젠트먼트윈도우·월청구모델·ISO취소전문.
|
||||
-- ===========================================================================
|
||||
SELECT setseed(0.61);
|
||||
|
||||
-- 1. 계정과목 (suspense/clearing 포함)
|
||||
INSERT INTO lg_account (acct_code, acct_name, acct_type, normal_side) VALUES
|
||||
('1010','현금및현금성자산','ASSET','D'),
|
||||
('1200','매입미수금','ASSET','D'),
|
||||
('1300','정산대기(clearing)','CLEARING','D'),
|
||||
('1900','미결계정(suspense)','SUSPENSE','D'),
|
||||
('2100','가맹점미지급금','LIABILITY','C'),
|
||||
('2200','발급사정산부채','LIABILITY','C'),
|
||||
('4100','MDR수수료수익','REVENUE','C'),
|
||||
('4200','채그백수수료수익','REVENUE','C'),
|
||||
('5100','interchange비용','EXPENSE','D'),
|
||||
('5200','scheme fee비용','EXPENSE','D')
|
||||
ON CONFLICT (acct_code) DO NOTHING;
|
||||
|
||||
-- 2. 가맹점 온보딩 이력 — 기존 200 가맹점 = 승인건 + 심사중/반려 추가.
|
||||
INSERT INTO mm_application (app_id, biz_no, name, mcc, rep_name, est_monthly, risk_score, status, reviewer, decision_at, merchant_id)
|
||||
SELECT nextval('mm_app_seq'),
|
||||
lpad(((('x'||substr(md5(m.merchant_id),1,6))::bit(24)::int)%900+100)::text,3,'0')||'-'||
|
||||
lpad(((('x'||substr(md5(m.merchant_id),7,2))::bit(8)::int)%90+10)::text,2,'0')||'-'||
|
||||
lpad(((('x'||substr(md5(m.merchant_id),9,5))::bit(20)::int)%90000+10000)::text,5,'0'),
|
||||
m.name, coalesce(mm.mcc,'5812'), '대표'||right(m.merchant_id,3),
|
||||
(('x'||substr(md5(m.merchant_id),3,6))::bit(24)::int)%200000000,
|
||||
(('x'||substr(md5(m.merchant_id),5,3))::bit(12)::int)%700,
|
||||
'APPROVED', '심사역'||((('x'||substr(md5(m.merchant_id),2,2))::bit(8)::int)%5+1),
|
||||
m.created_at + interval '3 days', m.merchant_id
|
||||
FROM merchant m LEFT JOIN mm_merch_mcc mm ON mm.merchant_id=m.merchant_id
|
||||
WHERE NOT EXISTS (SELECT 1 FROM mm_application a WHERE a.merchant_id=m.merchant_id);
|
||||
|
||||
-- 심사중/반려 신규 신청(미승인) 30건
|
||||
INSERT INTO mm_application (app_id, biz_no, name, mcc, rep_name, est_monthly, risk_score, status, reject_reason)
|
||||
SELECT nextval('mm_app_seq'),
|
||||
lpad((g%900+100)::text,3,'0')||'-'||lpad((g%90+10)::text,2,'0')||'-'||lpad((g*137%90000+10000)::text,5,'0'),
|
||||
'(신규신청) '||(ARRAY['한빛상사','블루오션','그린테크','스타몰','로얄샵'])[1+(g%5)]||' '||g||'호',
|
||||
(ARRAY['5812','5411','7011','4722','5732'])[1+(g%5)], '대표K'||g,
|
||||
(g*3300000)%300000000, (g*61)%1000,
|
||||
CASE WHEN g%3=0 THEN 'UNDERWRITING' WHEN g%7=0 THEN 'REJECTED' ELSE 'RECEIVED' END,
|
||||
CASE WHEN g%7=0 THEN '신용도 미달(리스크 초과)' ELSE NULL END
|
||||
FROM generate_series(1,30) g
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 3. KYC/AML 점검 (신청건마다 5종 점검)
|
||||
INSERT INTO mm_kyc (kyc_id, app_id, merchant_id, check_type, result, score, detail)
|
||||
SELECT nextval('mm_kyc_seq'), a.app_id, a.merchant_id, ct.t,
|
||||
CASE WHEN a.risk_score>850 AND ct.t IN ('AML','SANCTION') THEN 'REVIEW'
|
||||
WHEN a.status='REJECTED' AND ct.t='CREDIT' THEN 'FAIL' ELSE 'PASS' END,
|
||||
(a.risk_score + ((('x'||substr(md5(a.app_id::text||ct.t),1,3))::bit(12)::int)%200))%1000,
|
||||
ct.t||' 자동점검'
|
||||
FROM mm_application a CROSS JOIN (VALUES ('IDENTITY'),('SANCTION'),('PEP'),('AML'),('CREDIT')) ct(t)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM mm_kyc k WHERE k.app_id=a.app_id AND k.check_type=ct.t);
|
||||
|
||||
-- 4. 3자 대사 (순액정산 = 카드망, 내부원장 합, 은행입금). 대부분 일치, 소수 차이.
|
||||
INSERT INTO rc_3way (recon_id, biz_date, member_bin, amt_network, amt_internal, amt_bank, diff_ni, diff_nb, status)
|
||||
SELECT nextval('rc_3way_seq'), ns.biz_date, ns.member_bin,
|
||||
ns.net_amt AS amt_network,
|
||||
ns.net_amt AS amt_internal,
|
||||
CASE WHEN (ns.net_id % 97)=0 THEN ns.net_amt - 100 ELSE ns.net_amt END AS amt_bank,
|
||||
0,
|
||||
CASE WHEN (ns.net_id % 97)=0 THEN 100 ELSE 0 END,
|
||||
CASE WHEN (ns.net_id % 97)=0 THEN 'DIFF' ELSE 'MATCHED' END
|
||||
FROM st_net_settlement ns
|
||||
WHERE NOT EXISTS (SELECT 1 FROM rc_3way r WHERE r.biz_date=ns.biz_date AND r.member_bin=ns.member_bin);
|
||||
|
||||
-- 5. 프레젠트먼트 윈도우 / 지연 플래그 (승인~프레젠트 간격 기준)
|
||||
UPDATE cl_presentment c SET
|
||||
window_days = GREATEST(1, (c.present_date - p.biz_date)),
|
||||
late_flag = ((c.present_date - p.biz_date) > 3)
|
||||
FROM purchase p WHERE p.purchase_id=c.purchase_id AND c.window_days=1 AND c.late_flag=false
|
||||
AND (c.present_date - p.biz_date) <> 1;
|
||||
|
||||
-- 6. interchange downgrade 사유 (일부 거래 강등: 데이터품질/지연)
|
||||
UPDATE st_fee_component fc SET downgrade =
|
||||
CASE WHEN fc.purchase_id % 50 = 0 THEN 'LATE_PRESENT'
|
||||
WHEN fc.purchase_id % 73 = 0 THEN 'MISSING_AVS'
|
||||
WHEN fc.purchase_id % 91 = 0 THEN 'NO_3DS' END
|
||||
WHERE fc.downgrade IS NULL AND (fc.purchase_id % 50 = 0 OR fc.purchase_id % 73 = 0 OR fc.purchase_id % 91 = 0);
|
||||
|
||||
-- 7. 월청구 펀딩모델 활성화 (일부 대형가맹점 = MONTHLY_DISCOUNT, 총매출 입금·수수료 월말).
|
||||
UPDATE st_merchant_funding f SET funding_model='MONTHLY_DISCOUNT', deposit = gross_sales - refunds - chargebacks - reserve
|
||||
WHERE merchant_id IN (SELECT merchant_id FROM merchant WHERE daily_limit >= 300000000)
|
||||
AND funding_model='NET_DAILY';
|
||||
|
||||
-- 8. ISO 8583 취소(0420)/통지(0430) 전문 — 취소·무효 매입에 대응.
|
||||
INSERT INTO mg_msg_log (msg_id, stan, mti, channel, direction, proc_code, bitmap, amount, pan, mac, biz_date, created_at)
|
||||
SELECT (SELECT coalesce(max(msg_id),0) FROM mg_msg_log) + row_number() OVER (ORDER BY p.purchase_id),
|
||||
(p.purchase_id%100000), '0420',
|
||||
(ARRAY['CH01','CH02','CH03'])[1+(p.purchase_id%3)], 'OUT', '200000', '',
|
||||
p.amount, '4000-****-****-'||lpad((p.purchase_id%10000)::text,4,'0'), 'MAC'||substr(md5(p.purchase_id::text),1,8),
|
||||
p.biz_date, p.updated_at
|
||||
FROM purchase p WHERE p.status IN ('C','V') AND p.purchase_id < 10000000
|
||||
AND NOT EXISTS (SELECT 1 FROM mg_msg_log m WHERE m.mti='0420' LIMIT 1)
|
||||
LIMIT 20000;
|
||||
70
docs/OPS_RUNBOOK.md
Normal file
70
docs/OPS_RUNBOOK.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# 운영 · 비기능 설계 (Ops Runbook) — acquire-core-x
|
||||
|
||||
> 비기능 요건(HA/DR·PCI-DSS·데이터보존·처리량)은 픽스처에서 코드로 완전 구현할 수
|
||||
> 없는 인프라·조직 영역이다. 여기서는 **실제 매입시스템의 표준 설계를 명시**하고,
|
||||
> 현재 구현이 어디까지 충족하는지 정직하게 구분한다. (패리티 분석 §9 비기능 근거)
|
||||
|
||||
## 1. 처리량 (Throughput / TPS)
|
||||
|
||||
| 항목 | 실제 매입사 | 현재 구현 | 상태 |
|
||||
|---|---|---|---|
|
||||
| 승인 TPS | 수천~수만 TPS | XA 2PC 체인 커밋 0.2~0.5초/건(단일 게이트웨이) | ⚠️ 아키텍처는 동일(다중 서버카피), 부하생성 미검증 |
|
||||
| 서버 확장 | 서비스별 다중 인스턴스 | 모듈서버 min/max=4 카피(교착회피 실증) | ✅ |
|
||||
| 데이터 볼륨 | 수천만~억 행 | 1,130만 행(매입 135만, 5.5년) | ✅ 실전 규모대 |
|
||||
|
||||
- **관측된 실측**: 6단 XA 체인 커밋 0.2~0.5초, 체인 전수 스모크 477/477 최대 530ms.
|
||||
- **한계**: HTTP 게이트웨이가 단일 스레드(ATMI 컨텍스트 fork 비안전). 부하시험용 아님.
|
||||
실 부하는 tpcall 클라이언트 다중화(예: `acqdrv` 병렬)로 대체 측정 가능.
|
||||
|
||||
## 2. 고가용성 / 재해복구 (HA / DR)
|
||||
|
||||
**실제 매입사 표준**: Active-Active 이중센터, DB 동기복제(RPO≈0), 자동 페일오버(RTO<분),
|
||||
카드망 이중회선, 무정지 배포.
|
||||
|
||||
**현재 구현**:
|
||||
- Enduro/X `ndrxd` 가 서버 크래시시 자동 재기동(`restart_min/max`) — 프로세스 레벨 HA.
|
||||
- tmsrv XA 트랜잭션 로그(`tmlogs/`) 기반 커밋/롤백 복구 — 크래시 후 미결 2PC 정합 복구.
|
||||
- PostgreSQL 단일 인스턴스(스트리밍 복제 미구성).
|
||||
- **갭**: 다중센터·자동 페일오버·DB 동기복제는 인프라 영역으로 미구현(설계만 명시).
|
||||
|
||||
## 3. PCI-DSS 범위 (카드정보 보호)
|
||||
|
||||
**실제**: PAN 저장 금지/암호화, 토큰화로 스코프 축소, 키관리(HSM), 망분리, 접근통제.
|
||||
|
||||
**현재 구현 (Wave J)**:
|
||||
- ✅ **토큰볼트**(`card_vault`): PAN 원문 미저장, 마스킹(4000-****-****-1111) + SHA-256 해시만.
|
||||
- ✅ 서비스 `TOKENIZE` 가 원문 PAN 을 응답에 반환하지 않음.
|
||||
- ✅ 3DS/SCA·AVS/CVV 검증 계층.
|
||||
- **갭**: 실 HSM 키관리·전송구간 암호화(TLS 종단)·망분리는 인프라 영역.
|
||||
- **정직한 한계**: 시드 카드번호는 더미(Luhn 무관). 실 암호화 대신 해시 시연.
|
||||
|
||||
## 4. 감사추적 / 데이터 보존 (Audit / Retention)
|
||||
|
||||
| 항목 | 실제 | 현재 |
|
||||
|---|---|---|
|
||||
| 감사로그 | 전 서비스 처리이력, 불변 | ✅ `cm_log`(userlog 17,000+ 호출), 서비스별 진입/완료 |
|
||||
| 거래 이력 | 5~10년 보존(규제) | ✅ 5.5년치 매입 보존, 삭제 없음 |
|
||||
| 원장 불변성 | append-only 기표 | ✅ `ledger`/`lg_ledger_line` append(nextval 채번) |
|
||||
| 개인정보 보존정책 | 파기주기 관리 | ⚠️ 정책 테이블 미구현(설계만) |
|
||||
|
||||
## 5. 규제 준수 (Compliance)
|
||||
|
||||
- ✅ **여신전문금융업법 정산주기**: `rp_regulatory` 에 월별 평균정산일수(D+n) ≤ 3영업일 준수 보고.
|
||||
- ✅ **수수료 공시**: 3층 분해(interchange/scheme/markup)로 투명.
|
||||
- ✅ **원천징수/세금계산서**: `ac_wht`/`ac_tax_invoice`.
|
||||
- **갭**: 감독당국 실 보고 포맷(FSS 서식)·자금세탁방지(AML) 실 스크리닝은 KYC 시연 수준.
|
||||
|
||||
## 6. 요약 — 비기능 패리티
|
||||
|
||||
| 영역 | 상태 |
|
||||
|---|---|
|
||||
| 처리량·확장 아키텍처 | ✅ 실전 구조(다중 서버카피·XA·1,130만행) |
|
||||
| HA (프로세스) | ✅ ndrxd 자동재기동·XA 로그복구 |
|
||||
| DR (다중센터) | ⚠️ 설계 명시, 인프라 미구현 |
|
||||
| PCI (토큰볼트) | ✅ PAN 미저장·마스킹·해시 / ⚠️ HSM·망분리 인프라 |
|
||||
| 감사·보존 | ✅ 불변 로그·5.5년 이력 / ⚠️ 파기정책 |
|
||||
| 규제보고 | ✅ 여전법 정산주기·수수료공시 |
|
||||
|
||||
**결론**: 비기능은 **애플리케이션 레벨(감사·XA복구·토큰볼트·규제보고)은 실전급**,
|
||||
**인프라 레벨(다중센터 DR·HSM·망분리)은 설계 명시에 그침**. 후자는 픽스처 목적상
|
||||
코드가 아니라 배포 인프라의 몫이며, 마이그레이션 난이도에 대한 기여도 작다.
|
||||
|
|
@ -18,6 +18,17 @@
|
|||
> | 리포팅 | 70% | **85%** | 가맹점명세서·여전법 규제보고(134) |
|
||||
> | 비기능 | 35% | **50%** | 토큰볼트로 PCI 스코프 축소·PAN 원문 미저장 |
|
||||
> **가중 합계 ≈ 80 / 100** (6개 신규 서비스 전부 런타임 검증, 미결 2PC 0).
|
||||
>
|
||||
> ### Wave K 재채점 (잔여 갭 완결)
|
||||
> | 도메인 | 이전 | 현재 | 보강 내용 |
|
||||
> |---|---|---|---|
|
||||
> | 가맹점·마스터 | 75% | **90%** | 온보딩/언더라이팅(mm_application 230)·KYC/AML(mm_kyc 1,150)·MID발급 상태기계 + MM_ONBOARD/MM_KYC_CHECK |
|
||||
> | 매입·클리어링 | 75% | **85%** | 프레젠트먼트 윈도우·late_flag·interchange downgrade(56,912) + ISO0420 취소전문 |
|
||||
> | 원장·회계·대사 | 80% | **90%** | 계정과목(suspense/clearing 분리)·3자대사(rc_3way 16,185, 망·내부·은행) + RC_3WAY |
|
||||
> | 수수료 | 88% | **90%** | 월청구(MONTHLY_DISCOUNT) 펀딩모델 활성화(94,376) |
|
||||
> | 비기능 | 50% | **58%** | 운영 설계문서(OPS_RUNBOOK: HA/DR·PCI·보존·규제) 명시 |
|
||||
> **가중 합계 ≈ 87 / 100** — 애플리케이션 계층은 실전급, 잔여는 인프라(다중센터 DR·HSM·실카드망).
|
||||
> 마이그레이션 대상 픽스처로서의 실전성은 사실상 상한(~90%)에 도달.
|
||||
|
||||
## 도메인별 패리티 (가중 100점)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue