Wave G: 실제 매입사 경제모델 고도화 (딥리서치 갭분석 기반)
딥리서치(95 에이전트, OCC/Fed/Berlin Group 등 1차 출처, 검증된 9 finding)로 실제 카드 매입사 아키텍처를 조사(docs/GAP_ANALYSIS.md)하고, 최우선 갭 3개를 스키마+시드+런타임서비스+화면으로 메웠다. 메운 갭 (실제 매입사 핵심 메커니즘) 1. 수수료 3층 분해: 기존 flat MDR → interchange(발급사)+scheme fee(카드망)+ markup(매입사 수익). 실측 결과 interchange=MDR의 86% (리서치 "70~90%"와 일치). 2. 클리어링 ≠ 정산 분리: 승인·정산과 별도인 프레젠트먼트(클리어링) 단계 추가. 3. 발급사↔매입사 순액정산: BIN/ICA 기준 다자 순액(한국 BC카드 컨소시엄형). 4. 펀딩 항등식: 총매출−환불−채그백−수수료−리저브=입금 (2,924건 전부 항등 성립). 5. 채그백/분쟁 라이프사이클: FIRST_CB→REPRESENT→PRE_ARB→ARBITRATION 상태기계. 스키마 (db/schema.d/70-econ.sql, 96-econ-seed.sql) - 신규 9테이블: st_interchange_rate, st_scheme_fee, st_fee_component, cl_presentment, st_member_bank, st_net_settlement, st_reserve, st_merchant_funding, ac_dispute. - 기존 정산건 소급 분해(요율표 카드종류×채널 차등) + 순액정산 배치 + 펀딩 계산. - 시드 계산검증: 3층 항등식·펀딩 항등식 100% 성립(내가 채그백 일수중복 버그 1건 수정). 런타임 서비스 (전부 게이트웨이로 실호출 검증) - st: ST_FEEDECOMP(ic+sf+markup=mdr), ST_NETSETTLE(BIN별 순액), ST_FUNDING(항등식) - ac: ACQ_DISPUTE_OPEN, ACQ_DISPUTE_ADVANCE(상태기계-부적법 전이 거부), ACQ_PRESENT(클리어링). 각 120줄+, ECPG/XA 격리 규율 준수. 화면 (포털 AR0003 "정산 경제", #econ) - 수수료 3층 색상바 + 펀딩 항등식 워터폴 + 발급사별 순액정산 + 분쟁 라이프사이클 + 클리어링. 전부 실측 데이터 조회. 검증 - 통합 빌드 error 0, 도메인 재기동 5,879 서비스 AVAIL - 6개 신규 서비스 실호출 정상(수수료 3층 84=74+6+4, 상태기계 부적법 전이 거부) - 기존 6단 매입 체인 COMMIT OK 유지, 미결 2PC 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
46ff41b633
commit
438ae8992d
20 changed files with 1500 additions and 4 deletions
|
|
@ -155,6 +155,11 @@
|
|||
#include "ACQ_RETURNQRY.h"
|
||||
#include "ACQ_RETURNSTAT.h"
|
||||
|
||||
/* Wave G — 매입사 경제모델(분쟁 라이프사이클 + 클리어링) */
|
||||
#include "ACQ_DISPUTE_OPEN.h"
|
||||
#include "ACQ_DISPUTE_ADVANCE.h"
|
||||
#include "ACQ_PRESENT.h"
|
||||
|
||||
/* ----- advertise table: {service name, function} (fn is extern from its file) */
|
||||
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
|
||||
{"ACQUIRE", ACQUIRE}, {"ACQ_DDC", ACQ_DDC},
|
||||
|
|
@ -253,6 +258,9 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
|
|||
{"ACQ_EXCHANGE", ACQ_EXCHANGE},
|
||||
{"ACQ_RETURNQRY", ACQ_RETURNQRY},
|
||||
{"ACQ_RETURNSTAT", ACQ_RETURNSTAT},
|
||||
{"ACQ_DISPUTE_OPEN", ACQ_DISPUTE_OPEN},
|
||||
{"ACQ_DISPUTE_ADVANCE", ACQ_DISPUTE_ADVANCE},
|
||||
{"ACQ_PRESENT", ACQ_PRESENT},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
25
app/src/ac/svc/ACQ_DISPUTE_ADVANCE.h
Normal file
25
app/src/ac/svc/ACQ_DISPUTE_ADVANCE.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* ACQ_DISPUTE_ADVANCE.h - ac 서비스 ACQ_DISPUTE_ADVANCE 카피북 (copybook / record header).
|
||||
* 분쟁 라이프사이클 상태기계: FIRST_CB→REPRESENT→PRE_ARB→ARBITRATION→WON/LOST,
|
||||
* 어느 열린 단계에서든 ACCEPT→ACCEPTED. 부적절한 전이는 거부(FAIL).
|
||||
* Request fields: T_ID1(dispute_id), T_STR1(action), [T_STR2(현재 stage 힌트), T_PURCHASE_ID]
|
||||
* Response fields: T_STATUS(새 stage 또는 종결 status), T_ID1, T_ARG1(resolve flag), T_RC
|
||||
*/
|
||||
#ifndef ACQ_DISPUTE_ADVANCE_H
|
||||
#define ACQ_DISPUTE_ADVANCE_H
|
||||
|
||||
#include "ac_svc.h"
|
||||
|
||||
/* 분쟁 전이 결과 (내부 판정 결과 표현용) */
|
||||
typedef struct {
|
||||
long dispute_id;
|
||||
char from_stage[16];
|
||||
char action[16];
|
||||
char to_stage[16];
|
||||
char to_status[16];
|
||||
long resolved; /* 1 = 종결 전이(resolved_at 세팅) */
|
||||
} ac_dispute_move_t;
|
||||
|
||||
void ACQ_DISPUTE_ADVANCE(TPSVCINFO *p);
|
||||
|
||||
#endif /* ACQ_DISPUTE_ADVANCE_H */
|
||||
182
app/src/ac/svc/ACQ_DISPUTE_ADVANCE.pgc
Normal file
182
app/src/ac/svc/ACQ_DISPUTE_ADVANCE.pgc
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* ACQ_DISPUTE_ADVANCE.pgc - ac 모듈 서비스 ACQ_DISPUTE_ADVANCE (one service = one file).
|
||||
*
|
||||
* 분쟁 라이프사이클 상태기계. 카드 분쟁은 정해진 순서로만 진행하며, 이 서비스는
|
||||
* 현재 stage/status 를 읽어 요청 action 에 대한 "적법한 전이만" 허용하고 나머지는 거부한다.
|
||||
*
|
||||
* stage/action 전이표 (status='OPEN' 인 열린 분쟁에서만):
|
||||
* FIRST_CB --REPRESENT--> REPRESENT (매입사 재청구 방어)
|
||||
* REPRESENT --ESCALATE---> PRE_ARB (사전 중재 상신)
|
||||
* PRE_ARB --ESCALATE---> ARBITRATION (중재 상신)
|
||||
* ARBITRATION --WIN--------> status=WON (종결, resolved_at)
|
||||
* ARBITRATION --LOSE-------> status=LOST (종결, resolved_at)
|
||||
* <any open> --ACCEPT-----> status=ACCEPTED (책임 인수, 종결)
|
||||
* 그 외 (stage,action) 조합 = 부적절한 전이 → T_RC 세팅 후 FAIL.
|
||||
* 이미 WON/LOST/ACCEPTED 로 종결된 분쟁은 어떤 전이도 거부.
|
||||
*
|
||||
* 입출력 계약: 입력 T_ID1(dispute_id) + T_STR1(action)[+ T_STR2(현재 stage 힌트)],
|
||||
* 출력 T_STATUS(새 stage 또는 종결 status).
|
||||
* Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr.
|
||||
*/
|
||||
#include "ACQ_DISPUTE_ADVANCE.h"
|
||||
|
||||
void ACQ_DISPUTE_ADVANCE(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
const char *SVC = "ACQ_DISPUTE_ADVANCE";
|
||||
char action[24], bizdate[16], newstage[16], newstatus[16], memo[256], logmsg[256];
|
||||
long resolve = 0, valid = 0, i = 0;
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_did, h_pid, h_amt, h_logid, h_open;
|
||||
char h_stage[16], h_status[16], h_merch[64], h_reason[64];
|
||||
char h_new_stage[16], h_new_status[16], h_bizdate[16], h_memo[256], h_logmsg[256];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
|
||||
h_did = h_pid = h_amt = h_logid = h_open = 0;
|
||||
action[0] = bizdate[0] = newstage[0] = newstatus[0] = memo[0] = logmsg[0] = 0;
|
||||
h_stage[0] = h_status[0] = h_reason[0] = h_bizdate[0] = 0;
|
||||
strcpy(h_merch, "UNKNOWN");
|
||||
userlog("%s 진입: 분쟁 단계 전이 요청", SVC);
|
||||
|
||||
/* [1] 입력 검증부 -------------------------------------------------- */
|
||||
h_did = getl(b, T_ID1);
|
||||
if (h_did <= 0) {
|
||||
Bchg(b, T_MSG, 0, "분쟁 ID(T_ID1) 오류", 0L); setl(b, T_RC, 7301); FAIL(b);
|
||||
}
|
||||
gets_(b, T_STR1, action, sizeof(action));
|
||||
if (action[0] == 0) {
|
||||
Bchg(b, T_MSG, 0, "action(T_STR1) 필수", 0L); setl(b, T_RC, 7302); FAIL(b);
|
||||
}
|
||||
for (i = 0; action[i]; i++) /* action 대문자 정규화 */
|
||||
if (action[i] >= 'a' && action[i] <= 'z') action[i] -= 32;
|
||||
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
if (h_bizdate[0] == 0) {
|
||||
EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE';
|
||||
if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19");
|
||||
}
|
||||
|
||||
/* [2] 마스터 조회부: 현재 분쟁 상태 (전이의 기준) ------------------- */
|
||||
EXEC SQL SELECT purchase_id, merchant_id, amount, reason_code, stage, status
|
||||
INTO :h_pid, :h_merch, :h_amt, :h_reason, :h_stage, :h_status
|
||||
FROM ac_dispute WHERE dispute_id = :h_did;
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("%s: 분쟁 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
setl(b, T_RC, 90); FAIL(b);
|
||||
}
|
||||
if (sqlca.sqlcode == 100) {
|
||||
/* XA 격리: 형제 브랜치가 방금 개시한 미커밋 분쟁이면 전문에 실린 현재 stage 를 쓴다.
|
||||
* 전문 힌트(T_STR2)도 없으면 진짜 미존재이므로 업무검증 실패로 거부한다. */
|
||||
gets_(b, T_STR2, h_stage, sizeof(h_stage));
|
||||
gets_(b, T_STATUS, h_status, sizeof(h_status));
|
||||
if (h_status[0] == 0) strcpy(h_status, "OPEN");
|
||||
h_pid = getl(b, T_PURCHASE_ID);
|
||||
h_amt = getl(b, T_AMOUNT);
|
||||
if (h_stage[0] == 0) {
|
||||
userlog("%s: 분쟁 미존재 did=%ld (전문 힌트 없음)", SVC, h_did);
|
||||
Bchg(b, T_MSG, 0, "분쟁 미존재", 0L); setl(b, T_RC, 7303); FAIL(b);
|
||||
}
|
||||
userlog("%s: 분쟁 미커밋(체인) did=%ld — 전문 stage=%s 사용", SVC, h_did, h_stage);
|
||||
}
|
||||
userlog("%s 컨텍스트: did=%ld pid=%ld merch=%s stage=%s status=%s action=%s",
|
||||
SVC, h_did, h_pid, h_merch, h_stage, h_status, action);
|
||||
|
||||
/* [3] 종결 상태 확인부: 이미 resolved 면 어떤 전이도 불가 ----------- */
|
||||
if (strcmp(h_status, "WON") == 0 || strcmp(h_status, "LOST") == 0 ||
|
||||
strcmp(h_status, "ACCEPTED") == 0) {
|
||||
snprintf(logmsg, sizeof(logmsg), "이미 종결된 분쟁 status=%s 전이 거부", h_status);
|
||||
userlog("%s %s did=%ld", SVC, logmsg, h_did);
|
||||
Bchg(b, T_MSG, 0, logmsg, 0L);
|
||||
Bchg(b, T_STATUS, 0, h_status, 0L);
|
||||
setl(b, T_RC, 7310); FAIL(b);
|
||||
}
|
||||
|
||||
/* [4] 상태기계 전이 판정부 ----------------------------------------- */
|
||||
strcpy(newstage, h_stage); strcpy(newstatus, h_status);
|
||||
resolve = 0; valid = 0;
|
||||
if (strcmp(action, "ACCEPT") == 0) {
|
||||
/* 어느 열린 단계에서든 가맹점/매입사가 책임을 인수 → ACCEPTED(종결) */
|
||||
strcpy(newstatus, "ACCEPTED"); resolve = 1; valid = 1;
|
||||
} else if (strcmp(h_stage, "FIRST_CB") == 0 && strcmp(action, "REPRESENT") == 0) {
|
||||
strcpy(newstage, "REPRESENT"); valid = 1;
|
||||
} else if (strcmp(h_stage, "REPRESENT") == 0 && strcmp(action, "ESCALATE") == 0) {
|
||||
strcpy(newstage, "PRE_ARB"); valid = 1;
|
||||
} else if (strcmp(h_stage, "PRE_ARB") == 0 && strcmp(action, "ESCALATE") == 0) {
|
||||
strcpy(newstage, "ARBITRATION"); valid = 1;
|
||||
} else if (strcmp(h_stage, "ARBITRATION") == 0 &&
|
||||
(strcmp(action, "WIN") == 0 || strcmp(action, "WON") == 0)) {
|
||||
strcpy(newstatus, "WON"); resolve = 1; valid = 1;
|
||||
} else if (strcmp(h_stage, "ARBITRATION") == 0 &&
|
||||
(strcmp(action, "LOSE") == 0 || strcmp(action, "LOSS") == 0 ||
|
||||
strcmp(action, "LOST") == 0)) {
|
||||
strcpy(newstatus, "LOST"); resolve = 1; valid = 1;
|
||||
}
|
||||
if (!valid) {
|
||||
snprintf(logmsg, sizeof(logmsg), "부적절한 전이 거부: stage=%s action=%s", h_stage, action);
|
||||
userlog("%s %s did=%ld", SVC, logmsg, h_did);
|
||||
Bchg(b, T_MSG, 0, logmsg, 0L);
|
||||
Bchg(b, T_STATUS, 0, h_stage, 0L); /* 현재 stage 유지 통지 */
|
||||
setl(b, T_RC, 7320); FAIL(b);
|
||||
}
|
||||
userlog("%s 전이확정: %s/%s --%s--> stage=%s status=%s resolve=%ld",
|
||||
SVC, h_stage, h_status, action, newstage, newstatus, resolve);
|
||||
|
||||
/* [5] 본처리부: ac_dispute UPDATE (종결 전이면 resolved_at 세팅) ----- */
|
||||
strncpy(h_new_stage, newstage, sizeof(h_new_stage)-1); h_new_stage[sizeof(h_new_stage)-1] = 0;
|
||||
strncpy(h_new_status, newstatus, sizeof(h_new_status)-1); h_new_status[sizeof(h_new_status)-1] = 0;
|
||||
snprintf(memo, sizeof(memo), "전이 %s/%s -%s-> %s/%s @%s",
|
||||
h_stage, h_status, action, h_new_stage, h_new_status, h_bizdate);
|
||||
strncpy(h_memo, memo, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1] = 0;
|
||||
if (resolve) {
|
||||
EXEC SQL UPDATE ac_dispute
|
||||
SET stage = :h_new_stage, status = :h_new_status, resolved_at = now(), memo = :h_memo
|
||||
WHERE dispute_id = :h_did;
|
||||
} else {
|
||||
EXEC SQL UPDATE ac_dispute
|
||||
SET stage = :h_new_stage, status = :h_new_status, memo = :h_memo
|
||||
WHERE dispute_id = :h_did;
|
||||
}
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("%s: 분쟁 갱신 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
setl(b, T_RC, 90); FAIL(b);
|
||||
}
|
||||
if (sqlca.sqlcode == 100) {
|
||||
/* XA 격리: 대상 행이 이 브랜치에 안 보임(체인 미커밋). 전문 기반으로 진행하고 FAIL 안 함 */
|
||||
userlog("%s: UPDATE 대상 미가시(체인 미커밋) did=%ld — 전문값 응답", SVC, h_did);
|
||||
}
|
||||
|
||||
/* [6] 이력/감사 적재부 (cm_log) ------------------------------------- */
|
||||
snprintf(logmsg, sizeof(logmsg), "분쟁전이 did=%ld %s->%s/%s action=%s resolve=%ld",
|
||||
h_did, h_stage, h_new_stage, h_new_status, action, resolve);
|
||||
strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0;
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (:h_logid, 'ACQ_DISPUTE_ADVANCE', 'I', :h_logmsg, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
|
||||
/* [7] 집계/요약부: 가맹점 진행중 분쟁 수 재집계 --------------------- */
|
||||
EXEC SQL SELECT count(*) INTO :h_open FROM ac_dispute
|
||||
WHERE merchant_id = :h_merch AND status = 'OPEN';
|
||||
if (sqlca.sqlcode < 0) { h_open = 0; } /* 집계 실패는 응답을 막지 않음 */
|
||||
|
||||
/* [8] 응답부 (계약 보존: T_STATUS = 새 stage 또는 종결 status) ------- */
|
||||
if (resolve) Bchg(b, T_STATUS, 0, h_new_status, 0L);
|
||||
else Bchg(b, T_STATUS, 0, h_new_stage, 0L);
|
||||
setl(b, T_ID1, h_did);
|
||||
setl(b, T_PURCHASE_ID, h_pid);
|
||||
setl(b, T_AMOUNT, h_amt);
|
||||
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
|
||||
Bchg(b, T_STR2, 0, h_new_stage, 0L); /* 새 stage 힌트(체인 전달용) */
|
||||
Bchg(b, T_STR1, 0, action, 0L);
|
||||
setl(b, T_ARG1, resolve);
|
||||
setl(b, T_COUNT, h_open);
|
||||
Bchg(b, T_MSG, 0, h_memo, 0L);
|
||||
setl(b, T_RC, 0);
|
||||
userlog("%s 완료: did=%ld -> stage=%s status=%s (resolve=%ld)",
|
||||
SVC, h_did, h_new_stage, h_new_status, resolve);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
24
app/src/ac/svc/ACQ_DISPUTE_OPEN.h
Normal file
24
app/src/ac/svc/ACQ_DISPUTE_OPEN.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* ACQ_DISPUTE_OPEN.h - ac 서비스 ACQ_DISPUTE_OPEN 카피북 (copybook / record header).
|
||||
* 채그백/분쟁 개시: 매입 1건에 대해 ac_dispute 를 FIRST_CB/OPEN 으로 연다.
|
||||
* Request fields: T_PURCHASE_ID, T_STR1(reason_code), [T_AMOUNT, T_MERCHANT, T_BIZDATE]
|
||||
* Response fields: T_ID1(dispute_id), T_STATUS='FIRST_CB', T_AMOUNT, T_COUNT, T_ARG1(tier)
|
||||
*/
|
||||
#ifndef ACQ_DISPUTE_OPEN_H
|
||||
#define ACQ_DISPUTE_OPEN_H
|
||||
|
||||
#include "ac_svc.h"
|
||||
|
||||
typedef struct {
|
||||
long dispute_id;
|
||||
long purchase_id;
|
||||
char merchant_id[64];
|
||||
char reason_code[64];
|
||||
long amount;
|
||||
char stage[16]; /* FIRST_CB */
|
||||
char status[16]; /* OPEN */
|
||||
} ac_dispute_rec_t;
|
||||
|
||||
void ACQ_DISPUTE_OPEN(TPSVCINFO *p);
|
||||
|
||||
#endif /* ACQ_DISPUTE_OPEN_H */
|
||||
136
app/src/ac/svc/ACQ_DISPUTE_OPEN.pgc
Normal file
136
app/src/ac/svc/ACQ_DISPUTE_OPEN.pgc
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* ACQ_DISPUTE_OPEN.pgc - ac 모듈 서비스 ACQ_DISPUTE_OPEN (one service = one file).
|
||||
* 채그백/분쟁 개시: 매입 원장을 확인하고 ac_dispute 에 FIRST_CB/OPEN 분쟁행을 만든다.
|
||||
* 분쟁금액은 매입금액, reason_code 는 카드망 분쟁코드(10.4 사기 / 13.1 상품미수취 /
|
||||
* 12.6 중복 ...)를 그대로 적재하고 사기·상품·중복 등급(tier)으로 분류한다.
|
||||
* 입출력 계약: 입력 T_PURCHASE_ID + T_STR1(reason_code)[+ T_AMOUNT/T_MERCHANT/T_BIZDATE],
|
||||
* 출력 T_ID1(dispute_id) / T_STATUS='FIRST_CB'.
|
||||
* Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr.
|
||||
*/
|
||||
#include "ACQ_DISPUTE_OPEN.h"
|
||||
|
||||
void ACQ_DISPUTE_OPEN(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
const char *SVC = "ACQ_DISPUTE_OPEN";
|
||||
char bizdate[16], reason[64], memo[256], logmsg[256];
|
||||
long tier = 0;
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid, h_amt, h_did, h_logid, h_dup, h_cnt, h_merchcb;
|
||||
char h_bizdate[16], h_reason[64], h_merch[64], h_pstat[16];
|
||||
char h_memo[256], h_logmsg[256];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
|
||||
h_pid = h_amt = h_did = h_logid = h_dup = h_cnt = h_merchcb = 0;
|
||||
bizdate[0] = reason[0] = memo[0] = logmsg[0] = 0;
|
||||
h_bizdate[0] = h_reason[0] = 0;
|
||||
strcpy(h_merch, "UNKNOWN"); strcpy(h_pstat, "S");
|
||||
userlog("%s 진입: 분쟁/채그백 개시 요청", SVC);
|
||||
|
||||
/* [1] 입력 검증부 -------------------------------------------------- */
|
||||
h_pid = getl(b, T_PURCHASE_ID);
|
||||
if (h_pid <= 0) {
|
||||
Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 7201); FAIL(b);
|
||||
}
|
||||
gets_(b, T_STR1, reason, sizeof(reason));
|
||||
if (reason[0] == 0) strcpy(reason, "13.1"); /* 기본: 13.1 상품미수취 */
|
||||
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
|
||||
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
if (h_bizdate[0] == 0) {
|
||||
EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE';
|
||||
if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19");
|
||||
}
|
||||
|
||||
/* [2] 마스터/상태 조회부 (XA 격리: 미커밋이면 전문값 사용, 100 을 FAIL 로 다루지 않음) */
|
||||
EXEC SQL SELECT amount, merchant_id, status INTO :h_amt, :h_merch, :h_pstat
|
||||
FROM purchase WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("%s: 매입원장 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
setl(b, T_RC, 90); FAIL(b);
|
||||
}
|
||||
if (sqlca.sqlcode == 100) {
|
||||
/* 체인 내 신규 매입(미커밋): 요청 전문에 실린 값으로 진행 */
|
||||
h_amt = getl(b, T_AMOUNT);
|
||||
gets_(b, T_MERCHANT, h_merch, sizeof(h_merch));
|
||||
if (h_merch[0] == 0) strcpy(h_merch, "UNKNOWN");
|
||||
strcpy(h_pstat, "S");
|
||||
userlog("%s: 매입 미커밋(체인) pid=%ld — 전문값 사용", SVC, h_pid);
|
||||
}
|
||||
if (h_amt <= 0) h_amt = getl(b, T_AMOUNT);
|
||||
if (h_amt <= 0) {
|
||||
Bchg(b, T_MSG, 0, "분쟁 금액 산정 불가", 0L); setl(b, T_RC, 7202); FAIL(b);
|
||||
}
|
||||
userlog("%s 컨텍스트: pid=%ld merch=%s 상태=%s 금액=%ld reason=%s",
|
||||
SVC, h_pid, h_merch, h_pstat, h_amt, h_reason);
|
||||
|
||||
/* [3] 업무규칙 분기부: 중복 분쟁 방지 + reason 등급 분류 -------------- */
|
||||
EXEC SQL SELECT count(*) INTO :h_dup FROM ac_dispute
|
||||
WHERE purchase_id = :h_pid AND status = 'OPEN';
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
if (h_dup > 0) {
|
||||
userlog("%s 거절: 이미 진행중인 분쟁 존재 pid=%ld", SVC, h_pid);
|
||||
Bchg(b, T_MSG, 0, "이미 진행중인 분쟁 존재", 0L); setl(b, T_RC, 7203); FAIL(b);
|
||||
}
|
||||
/* 무효/취소된 매입은 분쟁 개시 자체가 성립하지 않는다 */
|
||||
if (strcmp(h_pstat, "V") == 0) {
|
||||
Bchg(b, T_MSG, 0, "무효 매입은 분쟁 불가", 0L); setl(b, T_RC, 7204); FAIL(b);
|
||||
}
|
||||
/* reason 등급: 사기(10.x)=3 최우선, 상품(13.x)=2, 중복(12.x)=1, 그 외=0 */
|
||||
if (strncmp(h_reason, "10.", 3) == 0) tier = 3;
|
||||
else if (strncmp(h_reason, "13.", 3) == 0) tier = 2;
|
||||
else if (strncmp(h_reason, "12.", 3) == 0) tier = 1;
|
||||
else tier = 0;
|
||||
|
||||
/* [4] 본처리부: dispute INSERT (FIRST_CB / OPEN) --------------------- */
|
||||
EXEC SQL SELECT nextval('ac_dispute_seq') INTO :h_did;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
snprintf(memo, sizeof(memo), "채그백 개시 pid=%ld reason=%s tier=%ld bizdate=%s",
|
||||
h_pid, h_reason, tier, h_bizdate);
|
||||
strncpy(h_memo, memo, sizeof(h_memo)-1); h_memo[sizeof(h_memo)-1] = 0;
|
||||
EXEC SQL INSERT INTO ac_dispute
|
||||
(dispute_id, purchase_id, merchant_id, reason_code, amount, stage, status, memo)
|
||||
VALUES (:h_did, :h_pid, :h_merch, :h_reason, :h_amt, 'FIRST_CB', 'OPEN', :h_memo);
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("%s: 분쟁행 적재 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
setl(b, T_RC, 90); FAIL(b);
|
||||
}
|
||||
|
||||
/* [5] 이력/감사 적재부 (cm_log) ------------------------------------- */
|
||||
snprintf(logmsg, sizeof(logmsg), "분쟁개시 did=%ld pid=%ld reason=%s amt=%ld tier=%ld",
|
||||
h_did, h_pid, h_reason, h_amt, tier);
|
||||
strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0;
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (:h_logid, 'ACQ_DISPUTE_OPEN', 'I', :h_logmsg, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
|
||||
/* [6] 집계/요약 갱신부: 가맹점 진행중 분쟁 수 + LOST 확정 채그백 누계 ---- */
|
||||
EXEC SQL SELECT count(*) INTO :h_cnt FROM ac_dispute
|
||||
WHERE merchant_id = :h_merch AND status = 'OPEN';
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_merchcb FROM ac_dispute
|
||||
WHERE merchant_id = :h_merch AND status = 'LOST';
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
userlog("%s 집계: merch=%s 진행중=%ld LOST누계=%ld", SVC, h_merch, h_cnt, h_merchcb);
|
||||
|
||||
/* [7] 응답부 (계약 보존: T_ID1=dispute_id / T_STATUS='FIRST_CB') ------ */
|
||||
setl(b, T_ID1, h_did);
|
||||
Bchg(b, T_STATUS, 0, "FIRST_CB", 0L);
|
||||
setl(b, T_PURCHASE_ID, h_pid);
|
||||
setl(b, T_AMOUNT, h_amt);
|
||||
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
|
||||
Bchg(b, T_STR1, 0, h_reason, 0L);
|
||||
Bchg(b, T_BIZDATE, 0, h_bizdate, 0L);
|
||||
setl(b, T_ARG1, tier);
|
||||
setl(b, T_COUNT, h_cnt);
|
||||
setl(b, T_GROSS, h_merchcb);
|
||||
Bchg(b, T_MSG, 0, "분쟁 개시(FIRST_CB/OPEN)", 0L);
|
||||
setl(b, T_RC, 0);
|
||||
userlog("%s 완료: did=%ld pid=%ld amt=%ld tier=%ld", SVC, h_did, h_pid, h_amt, tier);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
26
app/src/ac/svc/ACQ_PRESENT.h
Normal file
26
app/src/ac/svc/ACQ_PRESENT.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* ACQ_PRESENT.h - ac 서비스 ACQ_PRESENT 카피북 (copybook / record header).
|
||||
* 프레젠트먼트(클리어링) 생성: 승인·정산과 분리된 데이터 교환 단계.
|
||||
* clearing_amt = amount − interchange (발급사 귀속분 차감 후 매입사 클리어링액).
|
||||
* Request fields: T_PURCHASE_ID, [T_STR1(scheme), T_AMOUNT, T_AMT1(interchange), T_BIZDATE]
|
||||
* Response fields: T_ID1(presentment_id), T_AMT1(clearing_amt), T_STATUS='CLEARED', T_FEE(ic)
|
||||
*/
|
||||
#ifndef ACQ_PRESENT_H
|
||||
#define ACQ_PRESENT_H
|
||||
|
||||
#include "ac_svc.h"
|
||||
|
||||
typedef struct {
|
||||
long presentment_id;
|
||||
long purchase_id;
|
||||
char scheme[16];
|
||||
char msg_class[24]; /* FIRST_PRESENT / REPRESENT */
|
||||
long clearing_amt;
|
||||
char present_date[16];
|
||||
char cycle[12]; /* T+1 */
|
||||
char status[16]; /* CLEARED */
|
||||
} cl_presentment_rec_t;
|
||||
|
||||
void ACQ_PRESENT(TPSVCINFO *p);
|
||||
|
||||
#endif /* ACQ_PRESENT_H */
|
||||
140
app/src/ac/svc/ACQ_PRESENT.pgc
Normal file
140
app/src/ac/svc/ACQ_PRESENT.pgc
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* ACQ_PRESENT.pgc - ac 모듈 서비스 ACQ_PRESENT (one service = one file).
|
||||
* 프레젠트먼트(클리어링) 생성. 승인·정산과 분리된 데이터 교환 단계로, 발급사↔매입사
|
||||
* 채무를 확정한다. 수수료 3층 분해(st_fee_component)의 interchange 는 발급사 귀속분이므로
|
||||
* 클리어링 대상 금액은 clearing_amt = amount − interchange 이다.
|
||||
* 같은 매입에 FIRST_PRESENT 가 이미 있으면 msg_class 를 REPRESENT 로 승격한다.
|
||||
* 입출력 계약: 입력 T_PURCHASE_ID [+ T_STR1(scheme)/T_AMOUNT/T_AMT1(interchange)/T_BIZDATE],
|
||||
* 출력 T_ID1(presentment_id) / T_AMT1(clearing_amt) / T_STATUS='CLEARED'.
|
||||
* Runs on the caller's XA branch (no EXEC SQL CONNECT). Advertised by ac_svr.
|
||||
*/
|
||||
#include "ACQ_PRESENT.h"
|
||||
|
||||
void ACQ_PRESENT(TPSVCINFO *p)
|
||||
{
|
||||
UBFH *b = (UBFH *)p->data;
|
||||
const char *SVC = "ACQ_PRESENT";
|
||||
char bizdate[16], scheme[16], msgclass[24], cycle[12], logmsg[256];
|
||||
long clearing = 0, has_fc = 0;
|
||||
EXEC SQL BEGIN DECLARE SECTION;
|
||||
long h_pid, h_amt, h_ic, h_clr, h_prid, h_logid, h_cnt, h_prev;
|
||||
char h_bizdate[16], h_scheme[16], h_msgclass[24], h_cycle[12], h_merch[64], h_logmsg[256];
|
||||
EXEC SQL END DECLARE SECTION;
|
||||
|
||||
/* 지역/호스트 변수 초기화 (DECLARE SECTION 밖) */
|
||||
h_pid = h_amt = h_ic = h_clr = h_prid = h_logid = h_cnt = h_prev = 0;
|
||||
bizdate[0] = logmsg[0] = 0;
|
||||
strcpy(scheme, "BC"); strcpy(cycle, "T+1"); strcpy(msgclass, "FIRST_PRESENT");
|
||||
h_bizdate[0] = 0; strcpy(h_scheme, "BC"); strcpy(h_merch, "UNKNOWN");
|
||||
userlog("%s 진입: 프레젠트먼트(클리어링) 생성 요청", SVC);
|
||||
|
||||
/* [1] 입력 검증부 -------------------------------------------------- */
|
||||
h_pid = getl(b, T_PURCHASE_ID);
|
||||
if (h_pid <= 0) {
|
||||
Bchg(b, T_MSG, 0, "매입 ID 오류", 0L); setl(b, T_RC, 7401); FAIL(b);
|
||||
}
|
||||
gets_(b, T_STR1, scheme, sizeof(scheme));
|
||||
if (scheme[0] == 0) strcpy(scheme, "BC");
|
||||
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
|
||||
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
|
||||
|
||||
/* [2] 수수료 분해 조회부 (XA 격리: 없으면 전문 amount/interchange, 100 은 정상흐름) */
|
||||
EXEC SQL SELECT amount, interchange, scheme INTO :h_amt, :h_ic, :h_scheme
|
||||
FROM st_fee_component WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("%s: 수수료 분해 조회 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
setl(b, T_RC, 90); FAIL(b);
|
||||
}
|
||||
if (sqlca.sqlcode == 100) {
|
||||
/* 분해 미산출/미커밋: 전문값으로 클리어링. interchange 미제공이면 0 */
|
||||
has_fc = 0;
|
||||
h_amt = getl(b, T_AMOUNT);
|
||||
h_ic = getl(b, T_AMT1);
|
||||
strncpy(h_scheme, scheme, sizeof(h_scheme)-1); h_scheme[sizeof(h_scheme)-1] = 0;
|
||||
userlog("%s: st_fee_component 미존재 pid=%ld — 전문 amount=%ld ic=%ld 사용",
|
||||
SVC, h_pid, h_amt, h_ic);
|
||||
} else {
|
||||
has_fc = 1;
|
||||
}
|
||||
if (h_amt <= 0) h_amt = getl(b, T_AMOUNT);
|
||||
if (h_amt <= 0) {
|
||||
Bchg(b, T_MSG, 0, "클리어링 금액 산정 불가", 0L); setl(b, T_RC, 7402); FAIL(b);
|
||||
}
|
||||
if (h_ic < 0) h_ic = 0;
|
||||
if (h_ic > h_amt) h_ic = h_amt; /* interchange 는 매출을 넘지 못함 */
|
||||
if (h_scheme[0] == 0) strcpy(h_scheme, "BC");
|
||||
|
||||
/* 매입 원장에서 영업일/가맹점 보강 (미커밋이면 전문/파라미터 폴백) */
|
||||
EXEC SQL SELECT merchant_id, biz_date INTO :h_merch, :h_bizdate
|
||||
FROM purchase WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
if (sqlca.sqlcode == 100) {
|
||||
gets_(b, T_MERCHANT, h_merch, sizeof(h_merch));
|
||||
if (h_merch[0] == 0) strcpy(h_merch, "UNKNOWN");
|
||||
userlog("%s: 매입 미커밋(체인) pid=%ld — 전문값 사용", SVC, h_pid);
|
||||
}
|
||||
if (h_bizdate[0] == 0) {
|
||||
EXEC SQL SELECT param_val INTO :h_bizdate FROM cm_param WHERE param_key = 'BIZ_DATE';
|
||||
if (sqlca.sqlcode != 0) strcpy(h_bizdate, "2026-07-19");
|
||||
}
|
||||
userlog("%s 컨텍스트: pid=%ld merch=%s amt=%ld ic=%ld scheme=%s fc=%ld",
|
||||
SVC, h_pid, h_merch, h_amt, h_ic, h_scheme, has_fc);
|
||||
|
||||
/* [3] 업무규칙 분기부: 기존 프레젠트먼트 존재 여부로 msg_class 결정 ---- */
|
||||
EXEC SQL SELECT count(*) INTO :h_prev FROM cl_presentment
|
||||
WHERE purchase_id = :h_pid AND msg_class = 'FIRST_PRESENT';
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
if (h_prev > 0) strcpy(msgclass, "REPRESENT"); /* 1차가 있으면 재프레젠트 */
|
||||
else strcpy(msgclass, "FIRST_PRESENT");
|
||||
strncpy(h_msgclass, msgclass, sizeof(h_msgclass)-1); h_msgclass[sizeof(h_msgclass)-1] = 0;
|
||||
strncpy(h_cycle, cycle, sizeof(h_cycle)-1); h_cycle[sizeof(h_cycle)-1] = 0;
|
||||
|
||||
/* 클리어링 금액 = 매출 − 발급사 귀속 interchange */
|
||||
clearing = h_amt - h_ic;
|
||||
if (clearing < 0) clearing = 0;
|
||||
h_clr = clearing;
|
||||
|
||||
/* [4] 본처리부: cl_presentment INSERT (CLEARED) --------------------- */
|
||||
EXEC SQL SELECT nextval('cl_present_seq') INTO :h_prid;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
EXEC SQL INSERT INTO cl_presentment
|
||||
(presentment_id, purchase_id, scheme, msg_class, clearing_amt, present_date, cycle, status)
|
||||
VALUES (:h_prid, :h_pid, :h_scheme, :h_msgclass, :h_clr, :h_bizdate, :h_cycle, 'CLEARED');
|
||||
if (sqlca.sqlcode < 0) {
|
||||
userlog("%s: 프레젠트먼트 적재 실패 [%d] %s", SVC, sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
|
||||
setl(b, T_RC, 90); FAIL(b);
|
||||
}
|
||||
|
||||
/* [5] 이력/감사 적재부 (cm_log) ------------------------------------- */
|
||||
snprintf(logmsg, sizeof(logmsg), "프레젠트먼트 prid=%ld pid=%ld class=%s clr=%ld ic=%ld",
|
||||
h_prid, h_pid, h_msgclass, clearing, h_ic);
|
||||
strncpy(h_logmsg, logmsg, sizeof(h_logmsg)-1); h_logmsg[sizeof(h_logmsg)-1] = 0;
|
||||
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_logid;
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
|
||||
VALUES (:h_logid, 'ACQ_PRESENT', 'I', :h_logmsg, :h_bizdate);
|
||||
if (sqlca.sqlcode < 0) { setl(b, T_RC, 90); FAIL(b); }
|
||||
|
||||
/* [6] 집계/요약부: 해당 매입 프레젠트먼트 총 건수 ------------------- */
|
||||
EXEC SQL SELECT count(*) INTO :h_cnt FROM cl_presentment WHERE purchase_id = :h_pid;
|
||||
if (sqlca.sqlcode < 0) { h_cnt = 0; }
|
||||
|
||||
/* [7] 응답부 (계약 보존: T_ID1=presentment_id / T_AMT1=clearing_amt) - */
|
||||
setl(b, T_ID1, h_prid);
|
||||
setl(b, T_AMT1, clearing);
|
||||
setl(b, T_PURCHASE_ID, h_pid);
|
||||
setl(b, T_AMOUNT, h_amt);
|
||||
setl(b, T_FEE, h_ic);
|
||||
Bchg(b, T_STATUS, 0, "CLEARED", 0L);
|
||||
Bchg(b, T_STR1, 0, h_scheme, 0L);
|
||||
Bchg(b, T_STR2, 0, h_msgclass, 0L);
|
||||
Bchg(b, T_BIZDATE, 0, h_bizdate, 0L);
|
||||
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
|
||||
setl(b, T_COUNT, h_cnt);
|
||||
Bchg(b, T_MSG, 0, "프레젠트먼트 생성(CLEARED)", 0L);
|
||||
setl(b, T_RC, 0);
|
||||
userlog("%s 완료: prid=%ld pid=%ld clr=%ld class=%s", SVC, h_prid, h_pid, clearing, h_msgclass);
|
||||
OK(b);
|
||||
}
|
||||
|
||||
/* vim: set ts=4 sw=4 et smartindent: */
|
||||
Loading…
Add table
Add a link
Reference in a new issue