Phase 2b: 11개 모듈 전량 실동작 (333 서비스, 전서버 부팅)

- 10개 모듈 추가/확장(au py rc st lg cl mm vl mg cm), 각 30 서비스 = 330 + tmsrv
- build.sh 자동발견으로 11 모듈서버 + dbio 라이브러리 23 + 배치 22 빌드
- schema.d 11파일 → 75 테이블, 모듈별 disjoint
- 부팅 리소스 규명: fs.mqueue.queues_max=8192(330큐>256), NDRX_MSGSIZEMAX 16000·
  MSGMAX 50(큐당 5.6MB→0.8MB), msgqueue ulimit 2GB → 전 12서버 runok
- 검증: xadmin psc 333 AVAIL, 매입체인 XA 커밋, prepared_xacts=0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
forge-bot 2026-07-19 09:58:39 +00:00
parent 8219b85f14
commit bec83c721e
73 changed files with 10705 additions and 35 deletions

View file

@ -9,8 +9,8 @@ NDRX_IPCKEY=44100
NDRX_LDBAL=0
NDRX_LEV=5
NDRX_LOG=${NDRX_APPHOME}/log/xadmin.log
NDRX_MSGMAX=100
NDRX_MSGSIZEMAX=56000
NDRX_MSGMAX=50
NDRX_MSGSIZEMAX=16000
NDRX_NODEID=1
NDRX_QPATH=/dev/mqueue
NDRX_QPREFIX=/acq

View file

@ -35,17 +35,65 @@
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/ac_svr.log -r</sysopt>
</server>
<server name="rc_svr">
<server name="au_svr">
<srvid>110</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/au_svr.log -r</sysopt>
</server>
<server name="cl_svr">
<srvid>120</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/cl_svr.log -r</sysopt>
</server>
<server name="cm_svr">
<srvid>130</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/cm_svr.log -r</sysopt>
</server>
<server name="lg_svr">
<srvid>140</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/lg_svr.log -r</sysopt>
</server>
<server name="mg_svr">
<srvid>150</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/mg_svr.log -r</sysopt>
</server>
<server name="mm_svr">
<srvid>160</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/mm_svr.log -r</sysopt>
</server>
<server name="py_svr">
<srvid>170</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/py_svr.log -r</sysopt>
</server>
<server name="rc_svr">
<srvid>180</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/rc_svr.log -r</sysopt>
</server>
<server name="st_svr">
<srvid>120</srvid>
<srvid>190</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/st_svr.log -r</sysopt>
</server>
<server name="vl_svr">
<srvid>200</srvid>
<min>1</min>
<max>1</max>
<sysopt>-e ${NDRX_APPHOME}/log/vl_svr.log -r</sysopt>
</server>
</servers>
</endurox>

660
app/src/au/au_svr.pgc Normal file
View file

@ -0,0 +1,660 @@
/*
* au_svr.pgc - au (승인/한도) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online au services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct
* 승인/한도 operation with genuine EXEC SQL (directly, or via the linked
* libaudbio.a dbio layer) and common math from libacqcommon.a.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch); there is NO
* EXEC SQL CONNECT. Owner-writes discipline: services in this server never
* tpcall each other; each owns its tables on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "au_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void AUTH(TPSVCINFO *p); void AUTH_CANCEL(TPSVCINFO *p);
void AUTH_PARTCXL(TPSVCINFO *p); void LIMIT_CHK(TPSVCINFO *p);
void LIMIT_DEC(TPSVCINFO *p); void LIMIT_RST(TPSVCINFO *p);
void LIMIT_INQ(TPSVCINFO *p); void STANDIN(TPSVCINFO *p);
void FRAUD_DETECT(TPSVCINFO *p); void AUTH_RETRY(TPSVCINFO *p);
void INSTALL_AUTH(TPSVCINFO *p); void AUTH_STATUS(TPSVCINFO *p);
void AUTH_REVERSE(TPSVCINFO *p); void AUTH_APPROVE(TPSVCINFO *p);
void AUTH_DECLINE(TPSVCINFO *p); void CARD_VALIDATE(TPSVCINFO *p);
void VELOCITY_CHK(TPSVCINFO *p); void BLACKLIST_CHK(TPSVCINFO *p);
void AUTH_HOLD(TPSVCINFO *p); void AUTH_RELEASE(TPSVCINFO *p);
void AUTH_HISTORY(TPSVCINFO *p); void LIMIT_SET(TPSVCINFO *p);
void LIMIT_TEMP(TPSVCINFO *p); void AUTH_SUMMARY(TPSVCINFO *p);
void AUTH_ROUTE(TPSVCINFO *p); void DCC_QUOTE(TPSVCINFO *p);
void AUTH_CAPTURE(TPSVCINFO *p); void PREAUTH(TPSVCINFO *p);
void PREAUTH_COMPLETE(TPSVCINFO *p); void AUTH_VOID(TPSVCINFO *p);
/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"AUTH", AUTH}, {"AUTH_CANCEL", AUTH_CANCEL},
{"AUTH_PARTCXL", AUTH_PARTCXL}, {"LIMIT_CHK", LIMIT_CHK},
{"LIMIT_DEC", LIMIT_DEC}, {"LIMIT_RST", LIMIT_RST},
{"LIMIT_INQ", LIMIT_INQ}, {"STANDIN", STANDIN},
{"FRAUD_DETECT", FRAUD_DETECT}, {"AUTH_RETRY", AUTH_RETRY},
{"INSTALL_AUTH", INSTALL_AUTH}, {"AUTH_STATUS", AUTH_STATUS},
{"AUTH_REVERSE", AUTH_REVERSE}, {"AUTH_APPROVE", AUTH_APPROVE},
{"AUTH_DECLINE", AUTH_DECLINE}, {"CARD_VALIDATE", CARD_VALIDATE},
{"VELOCITY_CHK", VELOCITY_CHK}, {"BLACKLIST_CHK", BLACKLIST_CHK},
{"AUTH_HOLD", AUTH_HOLD}, {"AUTH_RELEASE", AUTH_RELEASE},
{"AUTH_HISTORY", AUTH_HISTORY}, {"LIMIT_SET", LIMIT_SET},
{"LIMIT_TEMP", LIMIT_TEMP}, {"AUTH_SUMMARY", AUTH_SUMMARY},
{"AUTH_ROUTE", AUTH_ROUTE}, {"DCC_QUOTE", DCC_QUOTE},
{"AUTH_CAPTURE", AUTH_CAPTURE}, {"PREAUTH", PREAUTH},
{"PREAUTH_COMPLETE", PREAUTH_COMPLETE}, {"AUTH_VOID", AUTH_VOID},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("au_svr: tpopen FAIL: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("au_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("au_svr: %d au(승인/한도) 서비스 광고 완료, RM 오픈", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("au_svr: tpsvrdone");
}
/* ========================================================================= */
/* 1. AUTH - 승인 접수: 한도 점검 후 authorization 원장 기록 + 한도 차감. */
/* ========================================================================= */
void AUTH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], merch[64], bizdate[16], appr[12];
long amount, auth_id, lim = 0, used = 0;
gets_(b, T_KEY1, card, sizeof(card));
if (card[0] == 0) { userlog("AUTH: T_KEY1(카드번호) 누락"); FAIL(b); }
gets_(b, T_MERCHANT, merch, sizeof(merch));
amount = getl(b, T_AMOUNT);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
/* 한도 점검: 초과면 거절(D) 상태로 기록 */
if (audb_limit_get(card, &lim, &used) < 0) FAIL(b);
if (used + amount > lim) {
userlog("AUTH 한도초과 card=%s used=%ld amt=%ld lim=%ld -> 거절", card, used, amount, lim);
auth_id = audb_next_auth_id();
if (auth_id < 0) FAIL(b);
if (audb_insert_auth(auth_id, card, merch, amount, "000000", "AUTH", "D", bizdate) < 0) FAIL(b);
setl(b, T_ID1, auth_id);
setl(b, T_RC, 1);
Bchg(b, T_STATUS, 0, "D", 0L);
OK(b);
}
auth_id = audb_next_auth_id();
if (auth_id < 0) FAIL(b);
sprintf(appr, "%06ld", auth_id % 1000000);
if (audb_insert_auth(auth_id, card, merch, amount, appr, "AUTH", "A", bizdate) < 0) FAIL(b);
if (audb_limit_dec(card, amount) < 0) FAIL(b);
setl(b, T_ID1, auth_id);
setl(b, T_RC, 0);
Bchg(b, T_STR2, 0, appr, 0L);
Bchg(b, T_STATUS, 0, "A", 0L);
userlog("AUTH 승인 auth_id=%ld card=%s merch=%s amt=%ld appr=%s", auth_id, card, merch, amount, appr);
OK(b);
}
/* 2. AUTH_CANCEL - 승인 전체 취소 + 한도 원복. */
void AUTH_CANCEL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], bizdate[16], reason[128];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_auth, h_amt;
char h_card[24], h_bizdate[16], h_reason[128];
EXEC SQL END DECLARE SECTION;
h_auth = getl(b, T_ID1);
h_amt = getl(b, T_AMOUNT);
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "CUSTOMER");
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date)
VALUES (:h_id, :h_auth, 'FULL', :h_amt, :h_reason, :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
if (audb_update_auth_status(h_auth, "C") < 0) FAIL(b);
if (card[0] && audb_limit_dec(card, -h_amt) < 0) FAIL(b);
setl(b, T_CANCEL_ID, h_id);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 3. AUTH_PARTCXL - 부분 취소: 승인 금액 감액 + 한도 부분 원복. */
void AUTH_PARTCXL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_auth, h_amt, h_new;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_auth = getl(b, T_ID1);
h_amt = getl(b, T_AMOUNT); /* 취소 금액 */
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT amount INTO :h_new FROM au_authorization WHERE auth_id = :h_auth;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
h_new = h_new - h_amt;
EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date)
VALUES (:h_id, :h_auth, 'PARTIAL', :h_amt, 'PARTIAL', :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
if (audb_update_auth_amount(h_auth, h_new) < 0) FAIL(b);
if (card[0] && audb_limit_dec(card, -h_amt) < 0) FAIL(b);
setl(b, T_CANCEL_ID, h_id);
setl(b, T_AMOUNT, h_new);
OK(b);
}
/* 4. LIMIT_CHK - 카드 한도 점검 (사용가능 여부만 리턴). */
void LIMIT_CHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
long amount, lim = 0, used = 0;
gets_(b, T_KEY1, card, sizeof(card));
amount = getl(b, T_AMOUNT);
if (audb_limit_get(card, &lim, &used) < 0) FAIL(b);
setl(b, T_AMT1, lim);
setl(b, T_AMT2, used);
setl(b, T_RC, (used + amount > lim) ? 1 : 0);
OK(b);
}
/* 5. LIMIT_DEC - 한도 차감(사용금액 증가). */
void LIMIT_DEC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
long amount = getl(b, T_AMOUNT);
gets_(b, T_KEY1, card, sizeof(card));
if (audb_limit_dec(card, amount) < 0) FAIL(b);
OK(b);
}
/* 6. LIMIT_RST - 한도 사용금액 초기화(일마감 원복). */
void LIMIT_RST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
gets_(b, T_KEY1, card, sizeof(card));
if (audb_limit_reset(card) < 0) FAIL(b);
OK(b);
}
/* 7. LIMIT_INQ - 한도 조회 (가용한도 계산). */
void LIMIT_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
long lim = 0, used = 0;
gets_(b, T_KEY1, card, sizeof(card));
if (audb_limit_get(card, &lim, &used) < 0) FAIL(b);
setl(b, T_AMT1, lim);
setl(b, T_AMT2, used);
setl(b, T_AMT3, lim - used); /* 가용한도 */
OK(b);
}
/* 8. STANDIN - 발급사 무응답 시 대리승인(STANDIN, 소액 한도 내). */
void STANDIN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], merch[64], bizdate[16];
long amount, auth_id, stand_lim = 50000;
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_MERCHANT, merch, sizeof(merch));
amount = getl(b, T_AMOUNT);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (amount > stand_lim) { setl(b, T_RC, 1); Bchg(b, T_STATUS, 0, "D", 0L); OK(b); }
auth_id = audb_next_auth_id();
if (auth_id < 0) FAIL(b);
if (audb_insert_auth(auth_id, card, merch, amount, "STDIN0", "STANDIN", "A", bizdate) < 0) FAIL(b);
setl(b, T_ID1, auth_id);
setl(b, T_RC, 0);
Bchg(b, T_STATUS, 0, "A", 0L);
OK(b);
}
/* 9. FRAUD_DETECT - 부정거래 스코어링 + 위험시 fraud_log 기록. */
void FRAUD_DETECT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], bizdate[16], reason[128];
long auth_id, amount, score = 0, fraud_id;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_card[24];
EXEC SQL END DECLARE SECTION;
auth_id = getl(b, T_ID1);
amount = getl(b, T_AMOUNT);
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
/* 룰: 고액 + 당일 승인건수 과다 = 스코어 상승 */
if (amount > 1000000) score += 50;
EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization WHERE card_no = :h_card;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt > 5) score += 40;
score += (h_cnt * 2);
strcpy(reason, score >= 50 ? "HIGH_RISK" : "OK");
setl(b, T_ARG1, score);
if (score >= 50) {
fraud_id = audb_insert_fraud(auth_id, card, score, reason, bizdate);
if (fraud_id < 0) FAIL(b);
setl(b, T_ID2, fraud_id);
setl(b, T_RC, 1);
} else {
setl(b, T_RC, 0);
}
OK(b);
}
/* 10. AUTH_RETRY - 거절 승인 재시도 (상태 D->A 재승인). */
void AUTH_RETRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
long auth_id = getl(b, T_ID1), amount = getl(b, T_AMOUNT), lim = 0, used = 0;
gets_(b, T_KEY1, card, sizeof(card));
if (audb_limit_get(card, &lim, &used) < 0) FAIL(b);
if (used + amount > lim) { setl(b, T_RC, 1); FAIL(b); }
if (audb_update_auth_status(auth_id, "A") < 0) FAIL(b);
if (audb_limit_dec(card, amount) < 0) FAIL(b);
setl(b, T_RC, 0);
Bchg(b, T_STATUS, 0, "A", 0L);
OK(b);
}
/* 11. INSTALL_AUTH - 할부 승인: 개월 수만큼 분할 승인 레코드. */
void INSTALL_AUTH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], merch[64], bizdate[16];
long months, amount, i, auth_id;
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_MERCHANT, merch, sizeof(merch));
amount = getl(b, T_AMOUNT);
months = getl(b, T_INSTALL_N); if (months < 1) months = 3;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
for (i = 1; i <= months; i++) {
auth_id = audb_next_auth_id();
if (auth_id < 0) FAIL(b);
if (audb_insert_auth(auth_id, card, merch, amount / months, "INST00", "INSTALL", "A", bizdate) < 0) FAIL(b);
}
if (audb_limit_dec(card, amount) < 0) FAIL(b);
setl(b, T_COUNT, months);
OK(b);
}
/* 12. AUTH_STATUS - 승인 상태 조회 (dbio). */
void AUTH_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long auth_id = getl(b, T_ID1);
char st[4];
if (audb_get_auth_status(auth_id, st) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, st, 0L);
OK(b);
}
/* 13. AUTH_REVERSE - 승인 역전(reversal) + 한도 원복. */
void AUTH_REVERSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
long auth_id = getl(b, T_ID1), amount = getl(b, T_AMOUNT);
gets_(b, T_KEY1, card, sizeof(card));
if (audb_update_auth_status(auth_id, "R") < 0) FAIL(b);
if (card[0] && audb_limit_dec(card, -amount) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 14. AUTH_APPROVE - 승인 확정(pending->approved). */
void AUTH_APPROVE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long auth_id = getl(b, T_ID1);
if (audb_update_auth_status(auth_id, "A") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "A", 0L);
OK(b);
}
/* 15. AUTH_DECLINE - 승인 거절 처리. */
void AUTH_DECLINE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char reason[128];
EXEC SQL BEGIN DECLARE SECTION;
long h_auth;
char h_reason[128];
EXEC SQL END DECLARE SECTION;
h_auth = getl(b, T_ID1);
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "DECLINE");
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
EXEC SQL UPDATE au_authorization SET status = 'D', decline_reason = :h_reason, updated_at = now()
WHERE auth_id = :h_auth;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_STATUS, 0, "D", 0L);
OK(b);
}
/* 16. CARD_VALIDATE - 카드번호 Luhn 검증 (common lib). */
void CARD_VALIDATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
gets_(b, T_KEY1, card, sizeof(card));
setl(b, T_RC, acq_luhn_ok(card) ? 0 : 1);
OK(b);
}
/* 17. VELOCITY_CHK - 속도(velocity) 점검: 당일 카드 승인건수/합계. */
void VELOCITY_CHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_sum;
char h_card[24], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum
FROM au_authorization WHERE card_no = :h_card AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_sum);
setl(b, T_RC, (h_cnt > 10) ? 1 : 0);
OK(b);
}
/* 18. BLACKLIST_CHK - 블랙리스트 카드 조회. */
void BLACKLIST_CHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_card[24];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, card, sizeof(card));
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM au_blacklist WHERE card_no = :h_card;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_RC, (h_cnt > 0) ? 1 : 0);
OK(b);
}
/* 19. AUTH_HOLD - 승인 보류. */
void AUTH_HOLD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long auth_id = getl(b, T_ID1);
if (audb_update_auth_status(auth_id, "H") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "H", 0L);
OK(b);
}
/* 20. AUTH_RELEASE - 보류 해제. */
void AUTH_RELEASE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long auth_id = getl(b, T_ID1);
if (audb_update_auth_status(auth_id, "A") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "A", 0L);
OK(b);
}
/* 21. AUTH_HISTORY - 카드 승인 이력 건수 조회. */
void AUTH_HISTORY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_card[24];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, card, sizeof(card));
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM au_authorization WHERE card_no = :h_card;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 22. LIMIT_SET - 카드 한도 설정/등록 (dbio upsert). */
void LIMIT_SET(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
long lim = getl(b, T_AMT1);
gets_(b, T_KEY1, card, sizeof(card));
if (lim <= 0) lim = 1000000;
if (audb_limit_set(card, lim) < 0) FAIL(b);
OK(b);
}
/* 23. LIMIT_TEMP - 임시 한도 상향 (temp_limit 저장 + 당일 상향). */
void LIMIT_TEMP(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24];
EXEC SQL BEGIN DECLARE SECTION;
long h_temp;
char h_card[24];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, card, sizeof(card));
h_temp = getl(b, T_AMT1); if (h_temp <= 0) h_temp = 2000000;
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL UPDATE card_limit
SET temp_limit = :h_temp, daily_limit = daily_limit + :h_temp, updated_at = now()
WHERE card_no = :h_card;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
OK(b);
}
/* 24. AUTH_SUMMARY - 가맹점 당일 승인 집계 upsert (dbio). */
void AUTH_SUMMARY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_appr, h_dec, h_amt;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*),
count(*) FILTER (WHERE status = 'A'),
count(*) FILTER (WHERE status = 'D'),
coalesce(sum(amount),0)
INTO :h_cnt, :h_appr, :h_dec, :h_amt
FROM au_authorization WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (audb_upsert_auth_summary(merch, bizdate, h_cnt, h_appr, h_dec, h_amt) < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
OK(b);
}
/* 25. AUTH_ROUTE - 승인 라우팅(발급사 BIN 기준 라우팅 코드 산출). */
void AUTH_ROUTE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], route[8];
long bin;
gets_(b, T_KEY1, card, sizeof(card));
bin = atol(card) / 1000000000L; /* 앞 6자리 근사 BIN */
if (card[0] == '4') strcpy(route, "VISA");
else if (card[0] == '5') strcpy(route, "MC");
else if (card[0] == '3') strcpy(route, "AMEX");
else strcpy(route, "LOCAL");
Bchg(b, T_STR1, 0, route, 0L);
setl(b, T_ARG1, bin);
OK(b);
}
/* 26. DCC_QUOTE - 해외승인 DCC 환산 견적(통화 -> 원화). */
void DCC_QUOTE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ccy[8];
long fx, rate = 1350, krw;
fx = getl(b, T_FX_AMT);
gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD");
krw = fx * rate / 100;
setl(b, T_AMOUNT, krw);
setl(b, T_ARG1, rate);
OK(b);
}
/* 27. AUTH_CAPTURE - 승인 매입확정(capture): 상태 A->M(매입대상). */
void AUTH_CAPTURE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_auth;
EXEC SQL END DECLARE SECTION;
h_auth = getl(b, T_ID1);
EXEC SQL UPDATE au_authorization SET status = 'M', captured_at = now(), updated_at = now()
WHERE auth_id = :h_auth AND status = 'A';
if (sqlca.sqlcode < 0) FAIL(b);
if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); }
setl(b, T_RC, 0);
Bchg(b, T_STATUS, 0, "M", 0L);
OK(b);
}
/* 28. PREAUTH - 예비승인(pre-authorization) 기록. */
void PREAUTH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], merch[64], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_amt;
char h_card[24], h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
h_amt = getl(b, T_AMOUNT);
strncpy(h_card, card, sizeof(h_card)-1); h_card[sizeof(h_card)-1] = 0;
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('au_preauth_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO au_preauth (preauth_id, card_no, merchant_id, amount, status, biz_date)
VALUES (:h_id, :h_card, :h_merch, :h_amt, 'P', :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_ID2, h_id);
OK(b);
}
/* 29. PREAUTH_COMPLETE - 예비승인 확정: preauth 소진 + 본승인 생성. */
void PREAUTH_COMPLETE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long auth_id;
EXEC SQL BEGIN DECLARE SECTION;
long h_pre, h_amt;
char h_card[24], h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pre = getl(b, T_ID2);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT card_no, merchant_id, amount INTO :h_card, :h_merch, :h_amt
FROM au_preauth WHERE preauth_id = :h_pre;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
EXEC SQL UPDATE au_preauth SET status = 'C' WHERE preauth_id = :h_pre;
if (sqlca.sqlcode < 0) FAIL(b);
auth_id = audb_next_auth_id();
if (auth_id < 0) FAIL(b);
if (audb_insert_auth(auth_id, h_card, h_merch, h_amt, "PRECMP", "PREAUTH", "A", bizdate) < 0) FAIL(b);
setl(b, T_ID1, auth_id);
OK(b);
}
/* 30. AUTH_VOID - 당일 승인 무효화(void) + 한도 원복. */
void AUTH_VOID(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char card[24], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_auth, h_amt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_auth = getl(b, T_ID1); h_amt = getl(b, T_AMOUNT);
gets_(b, T_KEY1, card, sizeof(card));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('au_cancel_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO au_cancel (cancel_id, auth_id, cancel_type, amount, reason, biz_date)
VALUES (:h_id, :h_auth, 'VOID', :h_amt, 'VOID', :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
if (audb_update_auth_status(h_auth, "V") < 0) FAIL(b);
if (card[0] && audb_limit_dec(card, -h_amt) < 0) FAIL(b);
setl(b, T_CANCEL_ID, h_id);
Bchg(b, T_STATUS, 0, "V", 0L);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,58 @@
/*
* au_fraud_scan_batch.pgc - au 부정거래 스캔 배치 (GROUP BY HAVING + XA).
*
* Groups the day's authorizations by card and, for cards whose승인건수/합계 that
* exceed velocity thresholds (HAVING), inserts a fraud_log row via the dbio
* layer - all under ONE global XA transaction. Usage: au_fraud_scan_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "au_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
char h_card[24];
long h_cnt, h_sum, h_score;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE fsc CURSOR FOR
SELECT card_no, count(*), coalesce(sum(amount),0)
FROM au_authorization
WHERE biz_date = :h_bizdate
GROUP BY card_no
HAVING count(*) > 5 OR coalesce(sum(amount),0) > 5000000
ORDER BY card_no;
EXEC SQL OPEN fsc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH fsc INTO :h_card, :h_cnt, :h_sum;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE fsc; tpabort(0); return 1; }
h_score = h_cnt * 10 + (h_sum > 5000000 ? 40 : 0);
if (audb_insert_fraud(0, h_card, h_score, "VELOCITY_BATCH", bizdate) < 0) {
EXEC SQL CLOSE fsc; tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE fsc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> au_fraud_scan_batch COMMIT: bizdate=%s flagged_cards=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,54 @@
/*
* au_settle_reflect_batch.pgc - au 승인 정산반영 배치 (커서 M->S + XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction, an EXEC SQL cursor scans captured authorizations (status='M')
* for a biz_date and flips each to settled (status='S') via the dbio layer,
* then tpcommit drives XA 2PC. Usage: au_settle_reflect_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "au_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_auth, h_amt;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE arc CURSOR FOR
SELECT auth_id, amount FROM au_authorization
WHERE biz_date = :h_bizdate AND status = 'M'
ORDER BY auth_id;
EXEC SQL OPEN arc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH arc INTO :h_auth, :h_amt;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE arc; tpabort(0); return 1; }
if (audb_update_auth_status(h_auth, "S") < 0) {
EXEC SQL CLOSE arc; tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE arc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> au_settle_reflect_batch COMMIT: bizdate=%s reflected(M->S)=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,121 @@
/*
* au_auth_dbio.pgc - 승인(authorization)/부정거래 DB 접근 (libaudbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "au_dbio.h"
long audb_next_auth_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('au_auth_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("audb_next_auth_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int audb_insert_auth(long auth_id, const char *card, const char *merch, long amount,
const char *appr_code, const char *auth_type, const char *status,
const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = auth_id, h_amount = amount;
char h_card[24], h_merch[64], h_appr[12], h_type[12], h_status[8], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_card, card, sizeof(h_card) - 1); h_card[sizeof(h_card)-1] = 0;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_appr, appr_code, sizeof(h_appr) - 1); h_appr[sizeof(h_appr)-1] = 0;
strncpy(h_type, auth_type, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO au_authorization
(auth_id, card_no, merchant_id, amount, approval_code, auth_type, status, biz_date)
VALUES (:h_id, :h_card, :h_merch, :h_amount, :h_appr, :h_type, :h_status, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("audb_insert_auth FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int audb_update_auth_status(long auth_id, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = auth_id;
char h_status[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE au_authorization SET status = :h_status, updated_at = now()
WHERE auth_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("audb_update_auth_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int audb_get_auth_status(long auth_id, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = auth_id;
char h_status[8];
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT status INTO :h_status FROM au_authorization WHERE auth_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("audb_get_auth_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
out[0] = h_status[0];
out[1] = 0;
return 0;
}
int audb_update_auth_amount(long auth_id, long amount)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = auth_id, h_amount = amount;
EXEC SQL END DECLARE SECTION;
EXEC SQL UPDATE au_authorization SET amount = :h_amount, updated_at = now()
WHERE auth_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("audb_update_auth_amount FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
long audb_insert_fraud(long auth_id, const char *card, long score, const char *reason,
const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_auth = auth_id, h_score = score;
char h_card[24], h_reason[128], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_card, card, sizeof(h_card) - 1); h_card[sizeof(h_card)-1] = 0;
strncpy(h_reason, reason, sizeof(h_reason) - 1); h_reason[sizeof(h_reason)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('au_fraud_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO au_fraud_log (fraud_id, auth_id, card_no, score, reason, biz_date)
VALUES (:h_id, :h_auth, :h_card, :h_score, :h_reason, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("audb_insert_fraud FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}

32
app/src/au/dbio/au_dbio.h Normal file
View file

@ -0,0 +1,32 @@
/*
* au_dbio.h - au (/) DB (libaudbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* au_svr and the au batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef AU_DBIO_H
#define AU_DBIO_H
/* authorization */
long audb_next_auth_id(void);
int audb_insert_auth(long auth_id, const char *card, const char *merch, long amount,
const char *appr_code, const char *auth_type, const char *status,
const char *bizdate);
int audb_update_auth_status(long auth_id, const char *status);
int audb_get_auth_status(long auth_id, char *out /* >= 2 bytes */);
int audb_update_auth_amount(long auth_id, long amount);
/* fraud */
long audb_insert_fraud(long auth_id, const char *card, long score, const char *reason,
const char *bizdate);
/* card limit */
int audb_limit_get(const char *card, long *limit_out, long *used_out);
int audb_limit_dec(const char *card, long amount);
int audb_limit_reset(const char *card);
int audb_limit_set(const char *card, long limit);
int audb_upsert_auth_summary(const char *merch, const char *bizdate, long cnt,
long approved, long declined, long amount);
#endif /* AU_DBIO_H */

View file

@ -0,0 +1,108 @@
/*
* au_limit_dbio.pgc - 카드 한도(card_limit) + 승인 일집계 DB 접근 (libaudbio.a).
*/
#include <string.h>
#include <userlog.h>
#include "au_dbio.h"
int audb_limit_get(const char *card, long *limit_out, long *used_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_limit, h_used;
char h_card[24];
EXEC SQL END DECLARE SECTION;
strncpy(h_card, card, sizeof(h_card) - 1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL SELECT daily_limit, used_amount INTO :h_limit, :h_used
FROM card_limit WHERE card_no = :h_card;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("audb_limit_get FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
*limit_out = h_limit;
*used_out = h_used;
return 0;
}
int audb_limit_dec(const char *card, long amount)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_amount = amount;
char h_card[24];
EXEC SQL END DECLARE SECTION;
strncpy(h_card, card, sizeof(h_card) - 1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL UPDATE card_limit SET used_amount = used_amount + :h_amount, updated_at = now()
WHERE card_no = :h_card;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("audb_limit_dec FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int audb_limit_reset(const char *card)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_card[24];
EXEC SQL END DECLARE SECTION;
strncpy(h_card, card, sizeof(h_card) - 1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL UPDATE card_limit SET used_amount = 0, updated_at = now()
WHERE card_no = :h_card;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("audb_limit_reset FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int audb_limit_set(const char *card, long limit)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_limit = limit;
char h_card[24];
EXEC SQL END DECLARE SECTION;
strncpy(h_card, card, sizeof(h_card) - 1); h_card[sizeof(h_card)-1] = 0;
EXEC SQL INSERT INTO card_limit (card_no, daily_limit, used_amount)
VALUES (:h_card, :h_limit, 0)
ON CONFLICT (card_no) DO UPDATE
SET daily_limit = EXCLUDED.daily_limit, updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("audb_limit_set FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int audb_upsert_auth_summary(const char *merch, const char *bizdate, long cnt,
long approved, long declined, long amount)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt = cnt, h_appr = approved, h_dec = declined, h_amt = amount;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO au_summary
(merchant_id, biz_date, auth_count, approve_count, decline_count, auth_amount)
VALUES (:h_merch, :h_bizdate, :h_cnt, :h_appr, :h_dec, :h_amt)
ON CONFLICT (merchant_id, biz_date) DO UPDATE
SET auth_count = au_summary.auth_count + EXCLUDED.auth_count,
approve_count = au_summary.approve_count + EXCLUDED.approve_count,
decline_count = au_summary.decline_count + EXCLUDED.decline_count,
auth_amount = au_summary.auth_amount + EXCLUDED.auth_amount,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("audb_upsert_auth_summary FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

View file

@ -0,0 +1,71 @@
/*
* cl_daily_batch.pgc - cl 일 마감 배치 (미마감 점검 + 커서 M->S + XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction. 먼저 당일 대사완료(M) 미마감 매입 건수를 점검하고, 커서로 각 건을
* 정산완료(S)로 마감 전환한 뒤, 당일 마감이력(close_log, DAILY)을 집계와 함께
* 기록한다. tpcommit 이 XA 2PC 를 구동. Usage: cl_daily_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "cl_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long closed = 0, cid;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_pid, h_unclosed, h_cnt, h_amt;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
/* 1) 미마감(대사완료 M) 점검 */
EXEC SQL SELECT count(*) INTO :h_unclosed FROM purchase
WHERE biz_date = :h_bizdate AND status = 'M';
if (sqlca.sqlcode < 0) { fprintf(stderr, "COUNT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
printf(">>> cl_daily_batch 미마감(M) 점검: bizdate=%s 대상=%ld건\n", bizdate, h_unclosed);
/* 2) 커서로 M -> S 마감 전환 */
EXEC SQL DECLARE cdc CURSOR FOR
SELECT purchase_id FROM purchase
WHERE biz_date = :h_bizdate AND status = 'M'
ORDER BY purchase_id;
EXEC SQL OPEN cdc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH cdc INTO :h_pid;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE cdc; tpabort(0); return 1; }
EXEC SQL UPDATE purchase SET status = 'S', updated_at = now() WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE cdc; tpabort(0); return 1; }
closed++;
}
EXEC SQL CLOSE cdc;
/* 3) 당일 집계 + 마감이력(DAILY) 기록 (via dbio) */
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt
FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) { tpabort(0); return 1; }
cid = cldb_next_close_id();
if (cid < 0) { tpabort(0); return 1; }
if (cldb_insert_close(cid, "DAILY", bizdate, bizdate, "C", h_cnt, h_amt, "일마감(배치)") < 0) {
tpabort(0); return 1;
}
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> cl_daily_batch COMMIT: bizdate=%s 마감전환=%ld cid=%ld 총건수=%ld\n",
bizdate, closed, cid, h_cnt);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,66 @@
/*
* cl_snapshot_batch.pgc - cl 마감 스냅샷 배치 (커서 + XA).
*
* 당일 마감(close_log, DAILY)에 대해 merchant_summary 를 가맹점별로 커서 순회하여
* cl_snapshot 에 고정 복사(스냅샷)한다. 마감 시점의 가맹점별 집계를 불변 기록으로
* 남긴다. 모두 ONE global XA transaction 하에서 수행.
* Usage: cl_snapshot_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "cl_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0, sid;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_merch[64];
long h_cid, h_gross, h_fee, h_net, h_cnt;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
/* 당일 최종 DAILY 마감 id 조회 (없으면 스냅샷 생략) */
EXEC SQL SELECT coalesce(max(close_id), 0) INTO :h_cid FROM cl_close_log
WHERE close_type = 'DAILY' AND biz_date = :h_bizdate AND status <> 'X';
if (sqlca.sqlcode < 0) { fprintf(stderr, "CID FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
if (h_cid == 0) {
printf(">>> cl_snapshot_batch: bizdate=%s DAILY 마감 없음 -> 스냅샷 생략\n", bizdate);
tpabort(0); tpclose(); tpterm(); return 0;
}
EXEC SQL DECLARE csc CURSOR FOR
SELECT merchant_id, gross_amount, fee_amount, net_amount, txn_count
FROM merchant_summary WHERE biz_date = :h_bizdate
ORDER BY merchant_id;
EXEC SQL OPEN csc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH csc INTO :h_merch, :h_gross, :h_fee, :h_net, :h_cnt;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE csc; tpabort(0); return 1; }
sid = cldb_next_snapshot_id();
if (sid < 0) { EXEC SQL CLOSE csc; tpabort(0); return 1; }
if (cldb_insert_snapshot(sid, h_cid, bizdate, bizdate, h_merch,
h_gross, h_fee, h_net, h_cnt) < 0) {
EXEC SQL CLOSE csc; tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE csc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> cl_snapshot_batch COMMIT: bizdate=%s cid=%ld 스냅샷=%ld건\n", bizdate, h_cid, rows);
tpclose(); tpterm();
return 0;
}

650
app/src/cl/cl_svr.pgc Normal file
View file

@ -0,0 +1,650 @@
/*
* cl_svr.pgc - cl (마감/closing) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online cl services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct 마감
* operation with genuine EXEC SQL (directly, or via the linked libcldbio.a dbio
* layer) and common helpers from libacqcommon.a.
*
* 마감 도메인: 일/월/분기/연 마감 이력(close_log), 가맹점별 마감 스냅샷(snapshot),
* 기간 잠금(period_lock). 마감 상태 O=개시 -> C=마감완료 -> A=승인 -> L=잠금,
* 취소 X / 재마감 R.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch libndrxxaecpg.so);
* there is NO EXEC SQL CONNECT. Each tpcall runs on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "cl_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void CL_DAILY(TPSVCINFO *p); void CL_MONTHLY(TPSVCINFO *p);
void CL_QUARTER(TPSVCINFO *p); void CL_SNAPSHOT(TPSVCINFO *p);
void CL_RECLOSE(TPSVCINFO *p); void CL_VERIFY(TPSVCINFO *p);
void CL_CANCEL(TPSVCINFO *p); void CL_STATS(TPSVCINFO *p);
void CL_BALVERIFY(TPSVCINFO *p); void CL_SEQ(TPSVCINFO *p);
void CL_REPORT(TPSVCINFO *p); void CL_ALERT(TPSVCINFO *p);
void CL_UNCLOSED(TPSVCINFO *p); void CL_APPROVE(TPSVCINFO *p);
void CL_LOCK(TPSVCINFO *p); void CL_UNLOCK(TPSVCINFO *p);
void CL_AGGREGATE(TPSVCINFO *p); void CL_DIFFCHK(TPSVCINFO *p);
void CL_SUMMARY(TPSVCINFO *p); void CL_STATUS(TPSVCINFO *p);
void CL_ROLLBACK(TPSVCINFO *p); void CL_CONFIRM(TPSVCINFO *p);
void CL_PERIOD(TPSVCINFO *p); void CL_YEAREND(TPSVCINFO *p);
void CL_MIDCLOSE(TPSVCINFO *p); void CL_RECON(TPSVCINFO *p);
void CL_ARCHIVE(TPSVCINFO *p); void CL_PURGE(TPSVCINFO *p);
void CL_RESTORE(TPSVCINFO *p); void CL_INQUIRY(TPSVCINFO *p);
/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"CL_DAILY", CL_DAILY}, {"CL_MONTHLY", CL_MONTHLY},
{"CL_QUARTER", CL_QUARTER}, {"CL_SNAPSHOT", CL_SNAPSHOT},
{"CL_RECLOSE", CL_RECLOSE}, {"CL_VERIFY", CL_VERIFY},
{"CL_CANCEL", CL_CANCEL}, {"CL_STATS", CL_STATS},
{"CL_BALVERIFY", CL_BALVERIFY}, {"CL_SEQ", CL_SEQ},
{"CL_REPORT", CL_REPORT}, {"CL_ALERT", CL_ALERT},
{"CL_UNCLOSED", CL_UNCLOSED}, {"CL_APPROVE", CL_APPROVE},
{"CL_LOCK", CL_LOCK}, {"CL_UNLOCK", CL_UNLOCK},
{"CL_AGGREGATE", CL_AGGREGATE}, {"CL_DIFFCHK", CL_DIFFCHK},
{"CL_SUMMARY", CL_SUMMARY}, {"CL_STATUS", CL_STATUS},
{"CL_ROLLBACK", CL_ROLLBACK}, {"CL_CONFIRM", CL_CONFIRM},
{"CL_PERIOD", CL_PERIOD}, {"CL_YEAREND", CL_YEAREND},
{"CL_MIDCLOSE", CL_MIDCLOSE}, {"CL_RECON", CL_RECON},
{"CL_ARCHIVE", CL_ARCHIVE}, {"CL_PURGE", CL_PURGE},
{"CL_RESTORE", CL_RESTORE}, {"CL_INQUIRY", CL_INQUIRY},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("cl_svr: tpopen FAIL: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("cl_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("cl_svr: %d cl 서비스 광고 완료, RM 개방", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("cl_svr: tpsvrdone");
}
/* 공통: 매입 집계 후 마감 이력 생성 헬퍼 (일/월/분기/연 공용). */
static int cl_open_close(UBFH *b, const char *ctype, const char *period,
const char *bizdate, long *cid_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt, h_cid;
char h_bizdate[16], h_type[16], h_period[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_type, ctype, sizeof(h_type)-1); h_type[sizeof(h_type)-1] = 0;
if (strcmp(ctype, "DAILY") == 0) {
EXEC SQL SELECT count(*), coalesce(sum(amount), 0) INTO :h_cnt, :h_amt
FROM purchase WHERE biz_date = :h_bizdate;
} else {
strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(amount), 0) INTO :h_cnt, :h_amt
FROM purchase WHERE to_char(biz_date, 'YYYY-MM') LIKE :h_period || '%';
}
if (sqlca.sqlcode < 0) return -1;
h_cid = cldb_next_close_id();
if (h_cid < 0) return -1;
if (cldb_insert_close(h_cid, ctype, period, bizdate, "C", h_cnt, h_amt, "마감") < 0) return -1;
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
*cid_out = h_cid;
return 0;
}
/* ========================================================================= */
/* 1. CL_DAILY - 일 마감: 당일 매입 집계 후 마감이력(DAILY) 생성. */
/* ========================================================================= */
void CL_DAILY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long cid = 0;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (cl_open_close(b, "DAILY", bizdate, bizdate, &cid) < 0) FAIL(b);
setl(b, T_ID1, cid);
Bchg(b, T_STATUS, 0, "C", 0L);
userlog("CL_DAILY cid=%ld bizdate=%s 일마감 완료", cid, bizdate);
OK(b);
}
/* 2. CL_MONTHLY - 월 마감: 해당 월(YYYY-MM) 매입 집계 후 마감이력(MONTHLY). */
void CL_MONTHLY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], period[16];
long cid = 0;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_STR1, period, sizeof(period));
if (period[0] == 0) { strncpy(period, bizdate, 7); period[7] = 0; }
if (cl_open_close(b, "MONTHLY", period, bizdate, &cid) < 0) FAIL(b);
setl(b, T_ID1, cid);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 3. CL_QUARTER - 분기 마감: 분기 프리픽스(YYYY-MM LIKE)로 집계. */
void CL_QUARTER(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], period[16];
long cid = 0;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_STR1, period, sizeof(period));
if (period[0] == 0) { strncpy(period, bizdate, 4); period[4] = 0; }
if (cl_open_close(b, "QUARTER", period, bizdate, &cid) < 0) FAIL(b);
setl(b, T_ID1, cid);
OK(b);
}
/* 4. CL_SNAPSHOT - 마감 스냅샷: merchant_summary 를 close_id 로 고정 복사. */
void CL_SNAPSHOT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], period[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_cnt;
char h_bizdate[16], h_period[16];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) strcpy(period, bizdate);
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0;
EXEC SQL INSERT INTO cl_snapshot
(snapshot_id, close_id, period_key, biz_date, merchant_id,
gross_amount, fee_amount, net_amount, txn_count)
SELECT nextval('cl_snapshot_seq'), :h_cid, :h_period, :h_bizdate,
ms.merchant_id, ms.gross_amount, ms.fee_amount, ms.net_amount, ms.txn_count
FROM merchant_summary ms WHERE ms.biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) { userlog("CL_SNAPSHOT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
h_cnt = sqlca.sqlerrd[2];
setl(b, T_COUNT, h_cnt);
userlog("CL_SNAPSHOT cid=%ld 가맹점 %ld건 스냅샷", h_cid, h_cnt);
OK(b);
}
/* 5. CL_RECLOSE - 재마감: 상태 R 로 표시하고 총계 재집계. */
void CL_RECLOSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_cnt, h_amt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt
FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) FAIL(b);
if (cldb_update_close_status(h_cid, "R") < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 6. CL_VERIFY - 마감 검증: close_log 총건수 vs 실제 매입건수 비교. */
void CL_VERIFY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_stored, h_actual;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT txn_count INTO :h_stored FROM cl_close_log WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_actual FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_actual);
setl(b, T_RC, (h_stored == h_actual) ? 0 : 1);
OK(b);
}
/* 7. CL_CANCEL - 마감 취소: 상태 X. */
void CL_CANCEL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long cid = getl(b, T_ID1);
if (cldb_update_close_status(cid, "X") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "X", 0L);
OK(b);
}
/* 8. CL_STATS - 마감 통계: close_type 별 마감 건수/총액. */
void CL_STATS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ctype[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt;
char h_type[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR2, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "DAILY");
strncpy(h_type, ctype, sizeof(h_type)-1); h_type[sizeof(h_type)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(total_amount),0) INTO :h_cnt, :h_amt
FROM cl_close_log WHERE close_type = :h_type AND status <> 'X';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
OK(b);
}
/* 9. CL_BALVERIFY - 잔액 검증: 스냅샷 net 합 vs close_log total 비교. */
void CL_BALVERIFY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_snap_net, h_total;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL SELECT coalesce(sum(net_amount),0) INTO :h_snap_net
FROM cl_snapshot WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT total_amount INTO :h_total FROM cl_close_log WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_AMT2, h_snap_net);
setl(b, T_AMT3, h_total);
setl(b, T_DELTA, h_total - h_snap_net);
OK(b);
}
/* 10. CL_SEQ - 마감 순번 발번(dbio upsert). */
void CL_SEQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ctype[16];
long seq;
gets_(b, T_STR2, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "DAILY");
seq = cldb_bump_seq(ctype);
if (seq < 0) FAIL(b);
setl(b, T_ID2, seq);
OK(b);
}
/* 11. CL_REPORT - 마감 리포트: 특정 마감의 스냅샷 건수/총매출. */
void CL_REPORT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_cnt, h_gross;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL SELECT count(*), coalesce(sum(gross_amount),0) INTO :h_cnt, :h_gross
FROM cl_snapshot WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_gross);
OK(b);
}
/* 12. CL_ALERT - 미마감 경보: 당일 대사완료(M) 미전기 매입 건수. */
void CL_ALERT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase
WHERE biz_date = :h_bizdate AND status = 'M';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, (h_cnt > 0) ? 1 : 0);
if (h_cnt > 0) userlog("CL_ALERT %s 미마감 %ld건 감지", bizdate, h_cnt);
OK(b);
}
/* 13. CL_UNCLOSED - 미마감일 점검: close_log 없는 매입 영업일 건수. */
void CL_UNCLOSED(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT count(DISTINCT p.biz_date) INTO :h_cnt
FROM purchase p
WHERE NOT EXISTS (SELECT 1 FROM cl_close_log c
WHERE c.close_type = 'DAILY' AND c.biz_date = p.biz_date
AND c.status <> 'X');
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 14. CL_APPROVE - 마감 승인: 상태 A. */
void CL_APPROVE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long cid = getl(b, T_ID1);
if (cldb_update_close_status(cid, "A") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "A", 0L);
OK(b);
}
/* 15. CL_LOCK - 기간 잠금: cl_period_lock upsert(locked=true) + 마감 L. */
void CL_LOCK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char period[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cid;
char h_period[16];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) gets_(b, T_BIZDATE, period, sizeof(period));
strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0;
EXEC SQL INSERT INTO cl_period_lock (period_key, locked) VALUES (:h_period, true)
ON CONFLICT (period_key) DO UPDATE SET locked = true, locked_at = now();
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cid > 0 && cldb_update_close_status(h_cid, "L") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "L", 0L);
OK(b);
}
/* 16. CL_UNLOCK - 기간 잠금 해제. */
void CL_UNLOCK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char period[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_period[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) gets_(b, T_BIZDATE, period, sizeof(period));
strncpy(h_period, period, sizeof(h_period)-1); h_period[sizeof(h_period)-1] = 0;
EXEC SQL UPDATE cl_period_lock SET locked = false, locked_at = now()
WHERE period_key = :h_period;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, sqlca.sqlerrd[2]);
OK(b);
}
/* 17. CL_AGGREGATE - 마감 총계 재집계 후 close_log 반영. */
void CL_AGGREGATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_cnt, h_amt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt
FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (cldb_update_close_totals(h_cid, h_cnt, h_amt) < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
OK(b);
}
/* 18. CL_DIFFCHK - 차액 점검: close_log total vs 스냅샷 gross 합. */
void CL_DIFFCHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_total, h_snap;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL SELECT total_amount INTO :h_total FROM cl_close_log WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
EXEC SQL SELECT coalesce(sum(gross_amount),0) INTO :h_snap
FROM cl_snapshot WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_DELTA, h_total - h_snap);
setl(b, T_RC, (h_total == h_snap) ? 0 : 1);
OK(b);
}
/* 19. CL_SUMMARY - 마감 요약: 스냅샷 gross/fee/net 합계 반환. */
void CL_SUMMARY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_gross, h_fee, h_net;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL SELECT coalesce(sum(gross_amount),0), coalesce(sum(fee_amount),0),
coalesce(sum(net_amount),0)
INTO :h_gross, :h_fee, :h_net FROM cl_snapshot WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_GROSS, h_gross);
setl(b, T_FEE, h_fee);
setl(b, T_NET, h_net);
OK(b);
}
/* 20. CL_STATUS - 마감 상태 조회(dbio getter). */
void CL_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long cid = getl(b, T_ID1);
char st[4];
if (cldb_get_close_status(cid, st) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, st, 0L);
OK(b);
}
/* 21. CL_ROLLBACK - 마감 롤백: 스냅샷 삭제 + 상태 O 로 복귀. */
void CL_ROLLBACK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL DELETE FROM cl_snapshot WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, sqlca.sqlerrd[2]);
if (cldb_update_close_status(h_cid, "O") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "O", 0L);
OK(b);
}
/* 22. CL_CONFIRM - 마감 확정: 상태 C. */
void CL_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long cid = getl(b, T_ID1);
if (cldb_update_close_status(cid, "C") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 23. CL_PERIOD - 기간 마감 개시(open): close_log 를 O 상태로 등록. */
void CL_PERIOD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], period[16], ctype[16];
long cid;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_STR1, period, sizeof(period)); if (period[0] == 0) strcpy(period, bizdate);
gets_(b, T_STR2, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "DAILY");
cid = cldb_next_close_id();
if (cid < 0) FAIL(b);
if (cldb_insert_close(cid, ctype, period, bizdate, "O", 0, 0, "기간개시") < 0) FAIL(b);
setl(b, T_ID1, cid);
Bchg(b, T_STATUS, 0, "O", 0L);
OK(b);
}
/* 24. CL_YEAREND - 연말 마감: 연도(YYYY) 프리픽스로 집계. */
void CL_YEAREND(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], period[16];
long cid = 0;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(period, bizdate, 4); period[4] = 0;
if (cl_open_close(b, "YEAREND", period, bizdate, &cid) < 0) FAIL(b);
setl(b, T_ID1, cid);
Bchg(b, T_STATUS, 0, "C", 0L);
userlog("CL_YEAREND cid=%ld 연도=%s 연마감 완료", cid, period);
OK(b);
}
/* 25. CL_MIDCLOSE - 중간 마감: 당일 중간 스냅 마감이력(MID) 생성. */
void CL_MIDCLOSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt, h_cid;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_amt
FROM purchase WHERE biz_date = :h_bizdate AND status IN ('A', 'M');
if (sqlca.sqlcode < 0) FAIL(b);
h_cid = cldb_next_close_id();
if (h_cid < 0) FAIL(b);
if (cldb_insert_close(h_cid, "MID", bizdate, bizdate, "O", h_cnt, h_amt, "중간마감") < 0) FAIL(b);
setl(b, T_ID1, h_cid);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 26. CL_RECON - 마감 대사: 실제 매입건수 vs close_log txn_count 대사. */
void CL_RECON(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_stored, h_actual, h_asum, h_tsum;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT txn_count, total_amount INTO :h_stored, :h_tsum
FROM cl_close_log WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_actual, :h_asum
FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_actual);
setl(b, T_DELTA, h_tsum - h_asum);
setl(b, T_RC, (h_stored == h_actual && h_tsum == h_asum) ? 0 : 1);
OK(b);
}
/* 27. CL_ARCHIVE - 마감 아카이브: 승인/완료 마감을 보관 상태로(memo 표시). */
void CL_ARCHIVE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL UPDATE cl_close_log
SET memo = 'ARCHIVED', updated_at = now()
WHERE close_id = :h_cid AND status IN ('C', 'A', 'L');
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_COUNT, sqlca.sqlerrd[2]);
OK(b);
}
/* 28. CL_PURGE - 취소 마감 정리: 오래된 X 상태 마감과 그 스냅샷 삭제. */
void CL_PURGE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_cnt;
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL DELETE FROM cl_snapshot WHERE close_id IN
(SELECT close_id FROM cl_close_log WHERE status = 'X' AND biz_date < :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL DELETE FROM cl_close_log WHERE status = 'X' AND biz_date < :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
h_cnt = sqlca.sqlerrd[2];
setl(b, T_COUNT, h_cnt);
userlog("CL_PURGE %s 이전 취소마감 %ld건 삭제", bizdate, h_cnt);
OK(b);
}
/* 29. CL_RESTORE - 마감 복원: 취소(X) 마감을 개시(O) 상태로 되돌림. */
void CL_RESTORE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid;
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL UPDATE cl_close_log SET status = 'O', memo = 'RESTORED', updated_at = now()
WHERE close_id = :h_cid AND status = 'X';
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_STATUS, 0, "O", 0L);
OK(b);
}
/* 30. CL_INQUIRY - 마감 조회: close_type/기간/상태/총건수 반환. */
void CL_INQUIRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cid, h_cnt, h_amt;
char h_type[16], h_status[8];
EXEC SQL END DECLARE SECTION;
h_cid = getl(b, T_ID1);
EXEC SQL SELECT close_type, status, txn_count, total_amount
INTO :h_type, :h_status, :h_cnt, :h_amt
FROM cl_close_log WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_STR2, 0, h_type, 0L);
Bchg(b, T_STATUS, 0, h_status, 0L);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,120 @@
/*
* cl_close_dbio.pgc - 마감이력(close_log) + 순번관리 DB 접근 (part of libcldbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "cl_dbio.h"
long cldb_next_close_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('cl_close_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("cldb_next_close_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int cldb_insert_close(long cid, const char *ctype, const char *period,
const char *bizdate, const char *status,
long txn_count, long total_amount, const char *memo)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cid = cid, h_cnt = txn_count, h_amt = total_amount;
char h_type[16], h_period[16], h_bizdate[16], h_status[8], h_memo[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, ctype, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_period, period, sizeof(h_period) - 1); h_period[sizeof(h_period)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_memo, memo, sizeof(h_memo) - 1); h_memo[sizeof(h_memo)-1] = 0;
EXEC SQL INSERT INTO cl_close_log
(close_id, close_type, period_key, biz_date, status, txn_count, total_amount, memo)
VALUES (:h_cid, :h_type, :h_period, :h_bizdate, :h_status, :h_cnt, :h_amt, :h_memo);
if (sqlca.sqlcode < 0) {
userlog("cldb_insert_close FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int cldb_update_close_status(long cid, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cid = cid;
char h_status[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE cl_close_log SET status = :h_status, updated_at = now()
WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("cldb_update_close_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int cldb_get_close_status(long cid, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cid = cid;
char h_status[8];
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT status INTO :h_status FROM cl_close_log WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("cldb_get_close_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
out[0] = h_status[0];
out[1] = 0;
return 0;
}
int cldb_update_close_totals(long cid, long txn_count, long total_amount)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cid = cid, h_cnt = txn_count, h_amt = total_amount;
EXEC SQL END DECLARE SECTION;
EXEC SQL UPDATE cl_close_log
SET txn_count = :h_cnt, total_amount = :h_amt, updated_at = now()
WHERE close_id = :h_cid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("cldb_update_close_totals FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
/* close_type 별 마지막 순번을 1 증가시켜 반환 (upsert). */
long cldb_bump_seq(const char *ctype)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_seq;
char h_type[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, ctype, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
EXEC SQL INSERT INTO cl_seq_reg (close_type, last_seq)
VALUES (:h_type, 1)
ON CONFLICT (close_type) DO UPDATE
SET last_seq = cl_seq_reg.last_seq + 1, updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("cldb_bump_seq FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
EXEC SQL SELECT last_seq INTO :h_seq FROM cl_seq_reg WHERE close_type = :h_type;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) return -1;
return h_seq;
}

28
app/src/cl/dbio/cl_dbio.h Normal file
View file

@ -0,0 +1,28 @@
/*
* cl_dbio.h - cl (/closing) DB (libcldbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* cl_svr and the cl batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef CL_DBIO_H
#define CL_DBIO_H
/* 마감 이력 (close_log) */
long cldb_next_close_id(void);
int cldb_insert_close(long cid, const char *ctype, const char *period,
const char *bizdate, const char *status,
long txn_count, long total_amount, const char *memo);
int cldb_update_close_status(long cid, const char *status);
int cldb_get_close_status(long cid, char *out /* >= 2 bytes */);
int cldb_update_close_totals(long cid, long txn_count, long total_amount);
/* 마감 스냅샷 (snapshot) */
long cldb_next_snapshot_id(void);
int cldb_insert_snapshot(long sid, long cid, const char *period, const char *bizdate,
const char *merch, long gross, long fee, long net, long cnt);
/* 마감 순번 관리 (seq registry) */
long cldb_bump_seq(const char *ctype);
#endif /* CL_DBIO_H */

View file

@ -0,0 +1,45 @@
/*
* cl_snapshot_dbio.pgc - 마감 스냅샷(snapshot) DB 접근 (part of libcldbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "cl_dbio.h"
long cldb_next_snapshot_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('cl_snapshot_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("cldb_next_snapshot_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int cldb_insert_snapshot(long sid, long cid, const char *period, const char *bizdate,
const char *merch, long gross, long fee, long net, long cnt)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_sid = sid, h_cid = cid, h_gross = gross, h_fee = fee, h_net = net, h_cnt = cnt;
char h_period[16], h_bizdate[16], h_merch[64];
EXEC SQL END DECLARE SECTION;
strncpy(h_period, period, sizeof(h_period) - 1); h_period[sizeof(h_period)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL INSERT INTO cl_snapshot
(snapshot_id, close_id, period_key, biz_date, merchant_id,
gross_amount, fee_amount, net_amount, txn_count)
VALUES (:h_sid, :h_cid, :h_period, :h_bizdate, :h_merch,
:h_gross, :h_fee, :h_net, :h_cnt);
if (sqlca.sqlcode < 0) {
userlog("cldb_insert_snapshot FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

View file

@ -0,0 +1,45 @@
/*
* cm_bizday_roll_batch.pgc - cm 영업일 롤 배치 (XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction. Reads cm_param BIZ_DATE, advances it to the next 영업일 (주말=common
* util로 skip, 휴일=cm_holiday로 skip), writes it back via the dbio layer, then
* tpcommit drives XA 2PC. Usage: cm_bizday_roll_batch
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "cm_dbio.h"
#include "acq_common.h"
int main(int argc, char **argv)
{
char cur[16], nxt[16];
int hol, guard = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (cmdb_get_param("BIZ_DATE", cur) < 0) {
fprintf(stderr, "BIZ_DATE 조회 실패\n"); tpabort(0); return 1;
}
/* 다음 영업일: 주말 + 휴일 모두 건너뜀 */
do {
if (acq_next_bizday(cur, nxt) < 0) { fprintf(stderr, "next_bizday 파싱 실패: %s\n", cur); tpabort(0); return 1; }
strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1] = 0;
hol = cmdb_is_holiday(nxt);
if (hol < 0) { tpabort(0); return 1; }
if (++guard > 30) break;
} while (hol == 1);
if (cmdb_set_param("BIZ_DATE", nxt) < 0) { tpabort(0); return 1; }
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> cm_bizday_roll_batch COMMIT: BIZ_DATE -> %s\n", nxt);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,58 @@
/*
* cm_purge_batch.pgc - cm 공통로그 보존기간 경과분 삭제 배치 (XA).
*
* ATMI client: opens the ECPG XA RM, begins ONE global transaction, deletes
* cm_log rows older than cm_param LOG_KEEP_DAYS (기본 90일) from a cutoff biz_date,
* then tpcommit drives XA 2PC. Usage: cm_purge_batch [YYYY-MM-DD(기준일)]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "cm_dbio.h"
int main(int argc, char **argv)
{
char keep_s[128];
long keep_days = 90;
EXEC SQL BEGIN DECLARE SECTION;
char h_asof[16];
long h_keep, h_deleted;
EXEC SQL END DECLARE SECTION;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
/* 보존일수는 파라미터에서 (없으면 90일 기본) */
if (cmdb_get_param("LOG_KEEP_DAYS", keep_s) == 0) {
long v = atol(keep_s);
if (v > 0) keep_days = v;
}
h_keep = keep_days;
/* 기준일: 인자 우선, 없으면 cm_param BIZ_DATE */
if (argc > 1) {
strncpy(h_asof, argv[1], sizeof(h_asof)-1); h_asof[sizeof(h_asof)-1] = 0;
} else if (cmdb_get_param("BIZ_DATE", keep_s) == 0) {
strncpy(h_asof, keep_s, sizeof(h_asof)-1); h_asof[sizeof(h_asof)-1] = 0;
} else {
strcpy(h_asof, "2026-07-19");
}
EXEC SQL DELETE FROM cm_log
WHERE biz_date < (CAST(:h_asof AS date) - (:h_keep || ' days')::interval);
if (sqlca.sqlcode < 0) {
fprintf(stderr, "DELETE FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
tpabort(0); return 1;
}
h_deleted = sqlca.sqlerrd[2];
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> cm_purge_batch COMMIT: asof=%s keep_days=%ld deleted=%ld\n",
h_asof, keep_days, h_deleted);
tpclose(); tpterm();
return 0;
}

554
app/src/cm/cm_svr.pgc Normal file
View file

@ -0,0 +1,554 @@
/*
* cm_svr.pgc - cm (공통 / common) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online cm services (the real Tuxedo
* "one module server, many services" pattern). Services are the shared utilities
* every other module leans on: 공통코드/파라미터 조회 (EXEC SQL against cm_code /
* cm_param), 영업일/휴일 계산, 금액 반올림·분할·포맷, 환율 환산, Luhn/CRC/체크디지트,
* 이자·일할·네팅 계산, 채번, 공통 로그. Compute-only services just Bget->compute->
* Bchg with no SQL; DB-backed ones use the linked libcmdbio.a layer or直접 EXEC SQL.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch libndrxxaecpg.so);
* there is NO EXEC SQL CONNECT. Each tpcall runs on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "cm_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
static void sets_(UBFH *b, BFLDID f, const char *s) { Bchg(b, f, 0, (char *)s, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- module-local pure-compute date helpers ---------------------------- */
static int cm_is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); }
static int cm_mdays(int y, int m)
{
static const int d[] = {31,28,31,30,31,30,31,31,30,31,30,31};
if (m < 1 || m > 12) return 30;
if (m == 2 && cm_is_leap(y)) return 29;
return d[m - 1];
}
/* Sakamoto: 0=Sun..6=Sat */
static int cm_dow(int y, int m, int d)
{
static int t[] = {0,3,2,5,0,3,5,1,4,6,2,4};
if (m < 3) y -= 1;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
/* CRC-16/CCITT-FALSE over a string. */
static unsigned int cm_crc16(const char *s)
{
unsigned int crc = 0xFFFF; int i, j;
for (i = 0; s[i]; i++) {
crc ^= (unsigned int)(unsigned char)s[i] << 8;
for (j = 0; j < 8; j++)
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF;
}
return crc & 0xFFFF;
}
/* ----- service prototypes ------------------------------------------------ */
void CM_CODE(TPSVCINFO *p); void CM_CODENAME(TPSVCINFO *p);
void CM_BIZDAY(TPSVCINFO *p); void CM_NEXTBIZ(TPSVCINFO *p);
void CM_ADDDAYS(TPSVCINFO *p); void CM_WEEKDAY(TPSVCINFO *p);
void CM_MONTHEND(TPSVCINFO *p); void CM_AMT_ROUND(TPSVCINFO *p);
void CM_AMT_SPLIT(TPSVCINFO *p); void CM_SEQ(TPSVCINFO *p);
void CM_FX(TPSVCINFO *p); void CM_FX_CONV(TPSVCINFO *p);
void CM_LUHN(TPSVCINFO *p); void CM_CRC(TPSVCINFO *p);
void CM_MASK(TPSVCINFO *p); void CM_FMT_DATE(TPSVCINFO *p);
void CM_FMT_AMT(TPSVCINFO *p); void CM_PARAM(TPSVCINFO *p);
void CM_PARAM_SET(TPSVCINFO *p); void CM_CACHE_LOAD(TPSVCINFO *p);
void CM_CACHE_INV(TPSVCINFO *p); void CM_ERRMSG(TPSVCINFO *p);
void CM_LOG(TPSVCINFO *p); void CM_HOLIDAY_CHK(TPSVCINFO *p);
void CM_CHECKDIGIT(TPSVCINFO *p); void CM_INTEREST(TPSVCINFO *p);
void CM_PRORATE(TPSVCINFO *p); void CM_NETTING_CALC(TPSVCINFO *p);
void CM_STATUS(TPSVCINFO *p); void CM_PING(TPSVCINFO *p);
/* ----- advertise table --------------------------------------------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"CM_CODE", CM_CODE}, {"CM_CODENAME", CM_CODENAME},
{"CM_BIZDAY", CM_BIZDAY}, {"CM_NEXTBIZ", CM_NEXTBIZ},
{"CM_ADDDAYS", CM_ADDDAYS}, {"CM_WEEKDAY", CM_WEEKDAY},
{"CM_MONTHEND", CM_MONTHEND}, {"CM_AMT_ROUND", CM_AMT_ROUND},
{"CM_AMT_SPLIT", CM_AMT_SPLIT}, {"CM_SEQ", CM_SEQ},
{"CM_FX", CM_FX}, {"CM_FX_CONV", CM_FX_CONV},
{"CM_LUHN", CM_LUHN}, {"CM_CRC", CM_CRC},
{"CM_MASK", CM_MASK}, {"CM_FMT_DATE", CM_FMT_DATE},
{"CM_FMT_AMT", CM_FMT_AMT}, {"CM_PARAM", CM_PARAM},
{"CM_PARAM_SET", CM_PARAM_SET}, {"CM_CACHE_LOAD", CM_CACHE_LOAD},
{"CM_CACHE_INV", CM_CACHE_INV}, {"CM_ERRMSG", CM_ERRMSG},
{"CM_LOG", CM_LOG}, {"CM_HOLIDAY_CHK", CM_HOLIDAY_CHK},
{"CM_CHECKDIGIT", CM_CHECKDIGIT},{"CM_INTEREST", CM_INTEREST},
{"CM_PRORATE", CM_PRORATE}, {"CM_NETTING_CALC", CM_NETTING_CALC},
{"CM_STATUS", CM_STATUS}, {"CM_PING", CM_PING},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("cm_svr: tpopen FAIL: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("cm_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("cm_svr: %d cm 공통 서비스 광고 완료, RM 오픈", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("cm_svr: tpsvrdone");
}
/* ========================================================================= */
/* 1. CM_CODE - 공통코드 존재 확인 (EXEC SQL via dbio). */
/* ========================================================================= */
void CM_CODE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32], code[32];
int ex;
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CCY");
gets_(b, T_STR2, code, sizeof(code));
ex = cmdb_code_exists(grp, code);
if (ex < 0) FAIL(b);
setl(b, T_RC, ex);
OK(b);
}
/* 2. CM_CODENAME - 공통코드 명 조회 (EXEC SQL). */
void CM_CODENAME(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32], code[32], name[128];
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CCY");
gets_(b, T_STR2, code, sizeof(code));
if (cmdb_get_code_name(grp, code, name) < 0) {
userlog("CM_CODENAME 미존재 grp=%s code=%s", grp, code);
FAIL(b);
}
sets_(b, T_STR3, name);
OK(b);
}
/* 3. CM_BIZDAY - 현재 영업일 조회 (cm_param BIZ_DATE). */
void CM_BIZDAY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char val[128];
if (cmdb_get_param("BIZ_DATE", val) < 0) FAIL(b);
sets_(b, T_BIZDATE, val);
OK(b);
}
/* 4. CM_NEXTBIZ - 다음 영업일 (주말 skip=common util + 휴일 skip=cm_holiday). */
void CM_NEXTBIZ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char in[16], cur[16], nxt[16];
int hol, guard = 0;
gets_(b, T_STR1, in, sizeof(in));
if (in[0] == 0) { if (cmdb_get_param("BIZ_DATE", in) < 0) strcpy(in, "2026-07-19"); }
strncpy(cur, in, sizeof(cur)-1); cur[sizeof(cur)-1] = 0;
do {
if (acq_next_bizday(cur, nxt) < 0) FAIL(b); /* 주말 건너뜀 */
strncpy(cur, nxt, sizeof(cur)-1); cur[sizeof(cur)-1] = 0;
hol = cmdb_is_holiday(nxt);
if (hol < 0) FAIL(b);
if (++guard > 30) break;
} while (hol == 1); /* 휴일이면 다시 +1영업일 */
sets_(b, T_STR3, nxt);
OK(b);
}
/* 5. CM_ADDDAYS - 달력일 N일 가산 (순수 계산). */
void CM_ADDDAYS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char in[16], out[16];
long n, i;
int y, m, d;
gets_(b, T_STR1, in, sizeof(in));
n = getl(b, T_ARG1);
if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b);
for (i = 0; i < n; i++) {
d++;
if (d > cm_mdays(y, m)) { d = 1; if (++m > 12) { m = 1; y++; } }
}
sprintf(out, "%04d-%02d-%02d", y, m, d);
sets_(b, T_STR3, out);
OK(b);
}
/* 6. CM_WEEKDAY - 요일 계산 (순수 계산): 0=일..6=토. */
void CM_WEEKDAY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
static const char *KOR[] = {"일","월","화","수","목","금","토"};
char in[16];
int y, m, d, w;
gets_(b, T_STR1, in, sizeof(in));
if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b);
w = cm_dow(y, m, d);
setl(b, T_ARG1, w);
sets_(b, T_STR3, KOR[w]);
setl(b, T_RC, (w == 0 || w == 6) ? 1 : 0); /* 1=주말 */
OK(b);
}
/* 7. CM_MONTHEND - 월말일 계산 (순수 계산). */
void CM_MONTHEND(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char in[16], out[16];
int y, m, d, last;
gets_(b, T_STR1, in, sizeof(in));
if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) FAIL(b);
last = cm_mdays(y, m);
sprintf(out, "%04d-%02d-%02d", y, m, last);
sets_(b, T_STR3, out);
setl(b, T_ARG1, last);
OK(b);
}
/* 8. CM_AMT_ROUND - 금액 반올림 (순수 계산): 단위(T_ARG1)로 반올림. */
void CM_AMT_ROUND(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long amt = getl(b, T_AMOUNT), unit = getl(b, T_ARG1), r;
if (unit <= 0) unit = 1;
r = ((amt + unit / 2) / unit) * unit;
setl(b, T_AMT1, r);
OK(b);
}
/* 9. CM_AMT_SPLIT - 금액 균등 분할 (순수 계산): n등분 + 잔액. */
void CM_AMT_SPLIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long amt = getl(b, T_AMOUNT), n = getl(b, T_ARG1), each, rem;
if (n < 1) n = 1;
each = amt / n;
rem = amt - each * n; /* 첫 회차에 잔액 가산 */
setl(b, T_AMT1, each);
setl(b, T_AMT2, rem);
setl(b, T_COUNT, n);
OK(b);
}
/* 10. CM_SEQ - 공통 채번 (cm_seq 이름있는 카운터). */
void CM_SEQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char name[32];
long v;
gets_(b, T_KEY1, name, sizeof(name)); if (name[0] == 0) strcpy(name, "BATCH");
v = cmdb_next_seq(name);
if (v < 0) FAIL(b);
setl(b, T_ARG1, v);
OK(b);
}
/* 11. CM_FX - 환율 조회 (cm_fx_rate). */
void CM_FX(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ccy[8], bizdate[16];
long rate;
gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); }
if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) { userlog("CM_FX 환율 없음 ccy=%s", ccy); FAIL(b); }
setl(b, T_ARG1, rate);
OK(b);
}
/* 12. CM_FX_CONV - 환율 환산 (cm_fx_rate + compute): 외화 -> 원화. */
void CM_FX_CONV(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ccy[8], bizdate[16];
long rate, fx, krw;
gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); }
fx = getl(b, T_FX_AMT);
if (cmdb_get_fx_rate(ccy, bizdate, &rate) < 0) FAIL(b);
krw = fx * rate / 100; /* rate_bps = KRW*100 per unit */
setl(b, T_AMOUNT, krw);
setl(b, T_ARG1, rate);
OK(b);
}
/* 13. CM_LUHN - 카드번호 Luhn 검증 (순수 계산, common util). */
void CM_LUHN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char pan[32];
gets_(b, T_STR5, pan, sizeof(pan));
setl(b, T_RC, acq_luhn_ok(pan) ? 0 : 1); /* 0=유효 */
OK(b);
}
/* 14. CM_CRC - CRC16 계산 (순수 계산). */
void CM_CRC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char in[256], out[8];
unsigned int crc;
gets_(b, T_STR1, in, sizeof(in));
crc = cm_crc16(in);
sprintf(out, "%04X", crc);
sets_(b, T_STR4, out);
setl(b, T_ARG1, (long)crc);
OK(b);
}
/* 15. CM_MASK - 카드번호 마스킹 (순수 계산): 앞6 뒤4 노출. */
void CM_MASK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char pan[32], out[32];
int n, i;
gets_(b, T_STR5, pan, sizeof(pan));
n = (int)strlen(pan);
for (i = 0; i < n && i < (int)sizeof(out) - 1; i++)
out[i] = (i < 6 || i >= n - 4) ? pan[i] : '*';
out[(n < (int)sizeof(out)-1) ? n : (int)sizeof(out)-1] = 0;
sets_(b, T_STR4, out);
OK(b);
}
/* 16. CM_FMT_DATE - 날짜 포맷 변환 (순수 계산): YYYYMMDD -> YYYY-MM-DD. */
void CM_FMT_DATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char in[16], out[16];
int y, m, d;
gets_(b, T_STR1, in, sizeof(in));
if (strlen(in) == 8 && sscanf(in, "%4d%2d%2d", &y, &m, &d) == 3)
sprintf(out, "%04d-%02d-%02d", y, m, d);
else if (sscanf(in, "%d-%d-%d", &y, &m, &d) == 3)
sprintf(out, "%04d%02d%02d", y, m, d); /* 역변환 */
else FAIL(b);
sets_(b, T_STR3, out);
OK(b);
}
/* 17. CM_FMT_AMT - 금액 천단위 콤마 포맷 (순수 계산). */
void CM_FMT_AMT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char raw[32], out[48];
long amt = getl(b, T_AMOUNT);
int n, i, j, k, neg = 0;
if (amt < 0) { neg = 1; amt = -amt; }
sprintf(raw, "%ld", amt);
n = (int)strlen(raw);
j = 0;
if (neg) out[j++] = '-';
for (i = 0; i < n; i++) {
out[j++] = raw[i];
k = n - i - 1;
if (k > 0 && k % 3 == 0) out[j++] = ',';
}
out[j] = 0;
sets_(b, T_STR4, out);
OK(b);
}
/* 18. CM_PARAM - 공통 파라미터 조회. */
void CM_PARAM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char key[64], val[128];
gets_(b, T_STR1, key, sizeof(key));
if (key[0] == 0) FAIL(b);
if (cmdb_get_param(key, val) < 0) { userlog("CM_PARAM 미존재 key=%s", key); FAIL(b); }
sets_(b, T_STR3, val);
OK(b);
}
/* 19. CM_PARAM_SET - 공통 파라미터 설정 (owner-writes, upsert). */
void CM_PARAM_SET(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char key[64], val[128];
gets_(b, T_STR1, key, sizeof(key));
gets_(b, T_STR3, val, sizeof(val));
if (key[0] == 0) FAIL(b);
if (cmdb_set_param(key, val) < 0) FAIL(b);
userlog("CM_PARAM_SET %s=%s", key, val);
OK(b);
}
/* 20. CM_CACHE_LOAD - 코드 그룹 캐시 로드 (건수 조회로 로딩 시뮬레이션). */
void CM_CACHE_LOAD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32];
long cnt;
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CCY");
cnt = cmdb_count_codes(grp);
if (cnt < 0) FAIL(b);
setl(b, T_COUNT, cnt);
userlog("CM_CACHE_LOAD grp=%s codes=%ld", grp, cnt);
OK(b);
}
/* 21. CM_CACHE_INV - 캐시 무효화 (순수 처리: 버전 증가 표시). */
void CM_CACHE_INV(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32];
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "ALL");
setl(b, T_RC, 0);
sets_(b, T_MSG, "캐시 무효화 완료");
userlog("CM_CACHE_INV grp=%s", grp);
OK(b);
}
/* 22. CM_ERRMSG - 오류코드 -> 메시지 매핑 (cm_code 그룹 STATUS/ERR 조회). */
void CM_ERRMSG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32], code[32], name[128];
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "STATUS");
gets_(b, T_STR2, code, sizeof(code));
if (cmdb_get_code_name(grp, code, name) < 0)
strcpy(name, "알 수 없는 코드"); /* 미등록 시 기본 메시지 */
sets_(b, T_MSG, name);
OK(b);
}
/* 23. CM_LOG - 공통 로그 기록 (cm_log). */
void CM_LOG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char svc[64], msg[256], bizdate[16];
long id;
gets_(b, T_SVCNAME, svc, sizeof(svc)); if (svc[0] == 0) strcpy(svc, "CM");
gets_(b, T_MSG, msg, sizeof(msg));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (bizdate[0] == 0) { if (cmdb_get_param("BIZ_DATE", bizdate) < 0) strcpy(bizdate, "2026-07-19"); }
id = cmdb_log(svc, "I", msg, bizdate);
if (id < 0) FAIL(b);
setl(b, T_ID1, id);
OK(b);
}
/* 24. CM_HOLIDAY_CHK - 휴일 여부 확인 (cm_holiday). */
void CM_HOLIDAY_CHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char date[16];
int h;
gets_(b, T_STR1, date, sizeof(date));
if (date[0] == 0) FAIL(b);
h = cmdb_is_holiday(date);
if (h < 0) FAIL(b);
setl(b, T_RC, h); /* 1=휴일, 0=평일 */
OK(b);
}
/* 25. CM_CHECKDIGIT - Luhn 체크디지트 생성 (순수 계산). */
void CM_CHECKDIGIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char base[32], cand[34];
int d, found = -1, n;
gets_(b, T_STR2, base, sizeof(base));
n = (int)strlen(base);
if (n == 0 || n >= (int)sizeof(cand) - 1) FAIL(b);
for (d = 0; d <= 9; d++) {
strcpy(cand, base);
cand[n] = (char)('0' + d);
cand[n + 1] = 0;
if (acq_luhn_ok(cand)) { found = d; break; }
}
if (found < 0) FAIL(b);
setl(b, T_ARG1, found);
sets_(b, T_STR4, cand); /* 체크디지트 포함 완성번호 */
OK(b);
}
/* 26. CM_INTEREST - 단리 이자 계산 (순수 계산): 원금*연이율(bps)*일수/(10000*365). */
void CM_INTEREST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long principal = getl(b, T_AMOUNT), rate_bps = getl(b, T_ARG1), days = getl(b, T_ARG2), interest;
if (principal < 0 || rate_bps < 0 || days < 0) FAIL(b);
interest = principal * rate_bps / 10000 * days / 365;
setl(b, T_AMT1, interest);
OK(b);
}
/* 27. CM_PRORATE - 일할 계산 (순수 계산): 금액*사용일/총일. */
void CM_PRORATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long amt = getl(b, T_AMOUNT), used = getl(b, T_ARG1), total = getl(b, T_ARG2), pro;
if (total <= 0) FAIL(b);
if (used < 0) used = 0;
if (used > total) used = total;
pro = amt * used / total;
setl(b, T_AMT1, pro);
OK(b);
}
/* 28. CM_NETTING_CALC - 네팅 계산 (순수 계산): 차변-대변. */
void CM_NETTING_CALC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long debit = getl(b, T_AMT1), credit = getl(b, T_AMT2), net;
net = debit - credit;
setl(b, T_NET, net);
setl(b, T_RC, net > 0 ? 1 : (net < 0 ? -1 : 0)); /* 1=수취, -1=지급 */
OK(b);
}
/* 29. CM_STATUS - 모듈 상태: 파라미터 건수 조회로 DB 연결 확인. */
void CM_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT count(*) INTO :h_cnt FROM cm_param;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, 0);
sets_(b, T_STATUS, "UP");
OK(b);
}
/* 30. CM_PING - 핑 (순수 처리). */
void CM_PING(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
sets_(b, T_MSG, "PONG");
setl(b, T_RC, 0);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,126 @@
/*
* cm_code_dbio.pgc - 공통코드/환율/휴일 DB 접근 (part of libcmdbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "cm_dbio.h"
int cmdb_get_code_name(const char *grp, const char *code, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_grp[32], h_code[32], h_name[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
strncpy(h_code, code, sizeof(h_code)-1); h_code[sizeof(h_code)-1] = 0;
EXEC SQL SELECT code_name INTO :h_name FROM cm_code
WHERE code_grp = :h_grp AND code = :h_code AND use_yn = 'Y';
if (sqlca.sqlcode == 100) { out[0] = 0; return -1; }
if (sqlca.sqlcode < 0) {
userlog("cmdb_get_code_name FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
strncpy(out, h_name, 127); out[127] = 0;
return 0;
}
int cmdb_code_exists(const char *grp, const char *code)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_grp[32], h_code[32];
EXEC SQL END DECLARE SECTION;
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
strncpy(h_code, code, sizeof(h_code)-1); h_code[sizeof(h_code)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM cm_code
WHERE code_grp = :h_grp AND code = :h_code;
if (sqlca.sqlcode < 0) {
userlog("cmdb_code_exists FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_cnt > 0 ? 1 : 0;
}
int cmdb_insert_code(const char *grp, const char *code, const char *name, long sort)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_sort = sort;
char h_grp[32], h_code[32], h_name[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
strncpy(h_code, code, sizeof(h_code)-1); h_code[sizeof(h_code)-1] = 0;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
EXEC SQL INSERT INTO cm_code (code_grp, code, code_name, sort_ord)
VALUES (:h_grp, :h_code, :h_name, :h_sort)
ON CONFLICT (code_grp, code) DO UPDATE
SET code_name = EXCLUDED.code_name, sort_ord = EXCLUDED.sort_ord, updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("cmdb_insert_code FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
long cmdb_count_codes(const char *grp)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_grp[32];
EXEC SQL END DECLARE SECTION;
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM cm_code
WHERE code_grp = :h_grp AND use_yn = 'Y';
if (sqlca.sqlcode < 0) {
userlog("cmdb_count_codes FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_cnt;
}
int cmdb_get_fx_rate(const char *ccy, const char *base_date, long *rate_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_rate;
char h_ccy[8], h_date[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0;
strncpy(h_date, base_date, sizeof(h_date)-1); h_date[sizeof(h_date)-1] = 0;
/* 요청일 이하 가장 최근 기준일 환율 */
EXEC SQL SELECT rate_bps INTO :h_rate FROM cm_fx_rate
WHERE ccy = :h_ccy AND base_date <= :h_date
ORDER BY base_date DESC LIMIT 1;
if (sqlca.sqlcode == 100) return -1;
if (sqlca.sqlcode < 0) {
userlog("cmdb_get_fx_rate FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
*rate_out = h_rate;
return 0;
}
int cmdb_is_holiday(const char *date)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_date[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_date, date, sizeof(h_date)-1); h_date[sizeof(h_date)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM cm_holiday WHERE holi_date = :h_date;
if (sqlca.sqlcode < 0) {
userlog("cmdb_is_holiday FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_cnt > 0 ? 1 : 0;
}

33
app/src/cm/dbio/cm_dbio.h Normal file
View file

@ -0,0 +1,33 @@
/*
* cm_dbio.h - cm () DB (libcmdbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* cm_svr and the cm batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error / not found (caller inspects sqlca).
*/
#ifndef CM_DBIO_H
#define CM_DBIO_H
/* 공통코드 (cm_code) */
int cmdb_get_code_name(const char *grp, const char *code, char *out /* >= 128 */);
int cmdb_code_exists(const char *grp, const char *code); /* 1=존재, 0=없음, -1=오류 */
int cmdb_insert_code(const char *grp, const char *code, const char *name, long sort);
long cmdb_count_codes(const char *grp);
/* 공통 파라미터 (cm_param) */
int cmdb_get_param(const char *key, char *out /* >= 128 */);
int cmdb_set_param(const char *key, const char *val);
/* 채번 (cm_seq) - 이름있는 카운터 +1 -> 새 값 */
long cmdb_next_seq(const char *name);
/* 환율 (cm_fx_rate) - 통화의 최신 기준일 rate_bps 조회 */
int cmdb_get_fx_rate(const char *ccy, const char *base_date, long *rate_out);
/* 휴일 (cm_holiday) - 1=휴일, 0=평일, -1=오류 */
int cmdb_is_holiday(const char *date);
/* 공통 로그 (cm_log) */
long cmdb_log(const char *svc, const char *level, const char *msg, const char *bizdate);
#endif /* CM_DBIO_H */

View file

@ -0,0 +1,93 @@
/*
* cm_param_dbio.pgc - 공통 파라미터/채번/공통로그 DB 접근 (part of libcmdbio.a).
*/
#include <string.h>
#include <userlog.h>
#include "cm_dbio.h"
int cmdb_get_param(const char *key, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_key[64], h_val[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_key, key, sizeof(h_key)-1); h_key[sizeof(h_key)-1] = 0;
EXEC SQL SELECT param_val INTO :h_val FROM cm_param WHERE param_key = :h_key;
if (sqlca.sqlcode == 100) { out[0] = 0; return -1; }
if (sqlca.sqlcode < 0) {
userlog("cmdb_get_param FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
strncpy(out, h_val, 127); out[127] = 0;
return 0;
}
int cmdb_set_param(const char *key, const char *val)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_key[64], h_val[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_key, key, sizeof(h_key)-1); h_key[sizeof(h_key)-1] = 0;
strncpy(h_val, val, sizeof(h_val)-1); h_val[sizeof(h_val)-1] = 0;
EXEC SQL INSERT INTO cm_param (param_key, param_val) VALUES (:h_key, :h_val)
ON CONFLICT (param_key) DO UPDATE
SET param_val = EXCLUDED.param_val, updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("cmdb_set_param FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
/* cm_seq 이름있는 카운터를 원자적으로 +1 하고 새 값을 돌려준다. */
long cmdb_next_seq(const char *name)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_val;
char h_name[32];
EXEC SQL END DECLARE SECTION;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
EXEC SQL UPDATE cm_seq SET seq_val = seq_val + 1, updated_at = now()
WHERE seq_name = :h_name;
if (sqlca.sqlcode < 0) {
userlog("cmdb_next_seq UPDATE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
if (sqlca.sqlcode == 100) { /* 최초 사용: 행 생성 */
EXEC SQL INSERT INTO cm_seq (seq_name, seq_val) VALUES (:h_name, 1);
if (sqlca.sqlcode < 0) return -1;
return 1;
}
EXEC SQL SELECT seq_val INTO :h_val FROM cm_seq WHERE seq_name = :h_name;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) return -1;
return h_val;
}
long cmdb_log(const char *svc, const char *level, const char *msg, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
char h_svc[64], h_level[4], h_msg[256], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_svc, svc, sizeof(h_svc)-1); h_svc[sizeof(h_svc)-1] = 0;
strncpy(h_level, level, sizeof(h_level)-1); h_level[sizeof(h_level)-1] = 0;
strncpy(h_msg, msg, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('cm_log_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date)
VALUES (:h_id, :h_svc, :h_level, :h_msg, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("cmdb_log FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}

View file

@ -0,0 +1,65 @@
/*
* lg_post_batch.pgc - lg 매입 원장 전기 배치 (커서 + XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction, an EXEC SQL cursor walks 대사완료(M) 상태 매입건을 훑어 각 건에
* 대해 매입 전기 전표(ACQ)를 복식부기로 생성한다:
* 차변 RECV = amount, 대변 MERCH_PAY = net, 대변 FEE_INC = fee.
* 전기 후 purchase.status M -> 'P'(posted) 로 표시, tpcommit 이 XA 2PC 를 구동.
* Usage: lg_post_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "lg_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0, vid;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_merch[64];
long h_pid, h_amount, h_fee, h_net;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE lpc CURSOR FOR
SELECT p.purchase_id, p.merchant_id, p.amount, p.fee, p.net
FROM purchase p
WHERE p.biz_date = :h_bizdate AND p.status = 'M'
ORDER BY p.purchase_id;
EXEC SQL OPEN lpc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH lpc INTO :h_pid, :h_merch, :h_amount, :h_fee, :h_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE lpc; tpabort(0); return 1; }
vid = lgdb_next_voucher_id();
if (vid < 0) { EXEC SQL CLOSE lpc; tpabort(0); return 1; }
if (lgdb_insert_voucher(vid, "ACQ", h_pid, "P", bizdate, "매입전기(배치)") < 0 ||
lgdb_insert_line(vid, "RECV", "D", h_amount, h_merch, bizdate, "posted") < 0 ||
lgdb_insert_line(vid, "MERCH_PAY", "C", h_net, h_merch, bizdate, "posted") < 0 ||
lgdb_insert_line(vid, "FEE_INC", "C", h_fee, h_merch, bizdate, "posted") < 0) {
EXEC SQL CLOSE lpc; tpabort(0); return 1;
}
EXEC SQL UPDATE purchase SET status = 'P', updated_at = now() WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE lpc; tpabort(0); return 1; }
rows++;
}
EXEC SQL CLOSE lpc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> lg_post_batch COMMIT: bizdate=%s 전기전표=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,63 @@
/*
* lg_trialbal_batch.pgc - lg 시산표(trial balance) 배치 (커서 + XA).
*
* 당일 원장 라인을 계정과목(acct)별로 GROUP BY 하여 차변/대변 합계를 산출하고
* lg_trial_bal 에 upsert 한다. 전체 대차 차액(diff) 을 누적해 마지막에 출력하여
* 복식부기 균형 여부를 검증한다. 모두 ONE global XA transaction 하에서 수행.
* Usage: lg_trialbal_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "lg_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0, net_diff = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_acct[32];
long h_dr, h_cr, h_diff;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE ltc CURSOR FOR
SELECT acct,
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0)
FROM lg_ledger_line WHERE biz_date = :h_bizdate
GROUP BY acct ORDER BY acct;
EXEC SQL OPEN ltc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH ltc INTO :h_acct, :h_dr, :h_cr;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE ltc; tpabort(0); return 1; }
h_diff = h_dr - h_cr;
EXEC SQL INSERT INTO lg_trial_bal (acct, biz_date, debit_total, credit_total, diff)
VALUES (:h_acct, :h_bizdate, :h_dr, :h_cr, :h_diff)
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = EXCLUDED.debit_total, credit_total = EXCLUDED.credit_total,
diff = EXCLUDED.diff, updated_at = now();
if (sqlca.sqlcode < 0) { fprintf(stderr, "UPSERT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE ltc; tpabort(0); return 1; }
net_diff += h_diff;
rows++;
}
EXEC SQL CLOSE ltc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> lg_trialbal_batch COMMIT: bizdate=%s 계정=%ld 총대차차액=%ld %s\n",
bizdate, rows, net_diff, (net_diff == 0) ? "(균형)" : "(불균형!)");
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,110 @@
/*
* lg_balance_dbio.pgc - 계정/가맹점 잔액 재계산 및 upsert (part of liblgdbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
* 복식부기 원칙: balance = 차변(D) 합 - 대변(C) 합.
*/
#include <string.h>
#include <userlog.h>
#include "lg_dbio.h"
/* 계정과목 x 영업일 잔액을 원장 라인에서 재집계하여 upsert. */
int lgdb_recalc_acct_bal(const char *acct, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_acct[32], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO lg_account_bal (acct, biz_date, debit_total, credit_total, balance)
SELECT :h_acct, :h_bizdate,
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE -amount END), 0)
FROM lg_ledger_line
WHERE acct = :h_acct AND biz_date = :h_bizdate
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = EXCLUDED.debit_total,
credit_total = EXCLUDED.credit_total,
balance = EXCLUDED.balance,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("lgdb_recalc_acct_bal FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int lgdb_get_acct_bal(const char *acct, const char *bizdate, long *bal_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_bal;
char h_acct[32], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT balance INTO :h_bal FROM lg_account_bal
WHERE acct = :h_acct AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("lgdb_get_acct_bal FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
*bal_out = h_bal;
return 0;
}
int lgdb_upsert_acct_bal(const char *acct, const char *bizdate, long debit, long credit)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_debit = debit, h_credit = credit, h_bal = debit - credit;
char h_acct[32], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO lg_account_bal (acct, biz_date, debit_total, credit_total, balance)
VALUES (:h_acct, :h_bizdate, :h_debit, :h_credit, :h_bal)
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = lg_account_bal.debit_total + EXCLUDED.debit_total,
credit_total = lg_account_bal.credit_total + EXCLUDED.credit_total,
balance = lg_account_bal.balance + EXCLUDED.balance,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("lgdb_upsert_acct_bal FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
/* 가맹점 x 영업일 잔액을 원장 라인에서 재집계하여 upsert. */
int lgdb_upsert_merch_bal(const char *merch, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO lg_merch_bal (merchant_id, biz_date, debit_total, credit_total, balance)
SELECT :h_merch, :h_bizdate,
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE -amount END), 0)
FROM lg_ledger_line
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate
ON CONFLICT (merchant_id, biz_date) DO UPDATE
SET debit_total = EXCLUDED.debit_total,
credit_total = EXCLUDED.credit_total,
balance = EXCLUDED.balance,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("lgdb_upsert_merch_bal FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

29
app/src/lg/dbio/lg_dbio.h Normal file
View file

@ -0,0 +1,29 @@
/*
* lg_dbio.h - lg (/) DB (liblgdbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* lg_svr and the lg batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef LG_DBIO_H
#define LG_DBIO_H
/* 전표 (voucher) */
long lgdb_next_voucher_id(void);
long lgdb_next_line_id(void);
int lgdb_insert_voucher(long vid, const char *vtype, long ref_id,
const char *status, const char *bizdate, const char *memo);
int lgdb_update_voucher_status(long vid, const char *status);
int lgdb_get_voucher_status(long vid, char *out /* >= 2 bytes */);
/* 원장 라인 (ledger_line) - 차변(D)/대변(C) 분개 */
int lgdb_insert_line(long vid, const char *acct, const char *dc, long amount,
const char *merch, const char *bizdate, const char *status);
/* 계정/가맹점 잔액 (balance) */
int lgdb_recalc_acct_bal(const char *acct, const char *bizdate);
int lgdb_get_acct_bal(const char *acct, const char *bizdate, long *bal_out);
int lgdb_upsert_acct_bal(const char *acct, const char *bizdate, long debit, long credit);
int lgdb_upsert_merch_bal(const char *merch, const char *bizdate);
#endif /* LG_DBIO_H */

View file

@ -0,0 +1,119 @@
/*
* lg_ledger_dbio.pgc - 전표/원장라인 DB 접근 (part of liblgdbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "lg_dbio.h"
long lgdb_next_voucher_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('lg_voucher_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("lgdb_next_voucher_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long lgdb_next_line_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('lg_line_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("lgdb_next_line_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int lgdb_insert_voucher(long vid, const char *vtype, long ref_id,
const char *status, const char *bizdate, const char *memo)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_vid = vid, h_ref = ref_id;
char h_type[16], h_status[8], h_bizdate[16], h_memo[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, vtype, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_memo, memo, sizeof(h_memo) - 1); h_memo[sizeof(h_memo)-1] = 0;
EXEC SQL INSERT INTO lg_voucher (voucher_id, voucher_type, ref_id, biz_date, status, memo)
VALUES (:h_vid, :h_type, :h_ref, :h_bizdate, :h_status, :h_memo);
if (sqlca.sqlcode < 0) {
userlog("lgdb_insert_voucher FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int lgdb_update_voucher_status(long vid, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_vid = vid;
char h_status[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE lg_voucher SET status = :h_status, updated_at = now()
WHERE voucher_id = :h_vid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("lgdb_update_voucher_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int lgdb_get_voucher_status(long vid, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_vid = vid;
char h_status[8];
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT status INTO :h_status FROM lg_voucher WHERE voucher_id = :h_vid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("lgdb_get_voucher_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
out[0] = h_status[0];
out[1] = 0;
return 0;
}
int lgdb_insert_line(long vid, const char *acct, const char *dc, long amount,
const char *merch, const char *bizdate, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_vid = vid, h_amount = amount;
char h_acct[32], h_dc[4], h_merch[64], h_bizdate[16], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_dc, dc, sizeof(h_dc) - 1); h_dc[sizeof(h_dc)-1] = 0;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL SELECT nextval('lg_line_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO lg_ledger_line
(line_id, voucher_id, acct, dc, amount, merchant_id, biz_date, status)
VALUES (:h_id, :h_vid, :h_acct, :h_dc, :h_amount, :h_merch, :h_bizdate, :h_status);
if (sqlca.sqlcode < 0) {
userlog("lgdb_insert_line FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

703
app/src/lg/lg_svr.pgc Normal file
View file

@ -0,0 +1,703 @@
/*
* lg_svr.pgc - lg (원장/ledger) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online lg services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct 원장
* (복식부기) operation with genuine EXEC SQL (directly, or via the linked
* liblgdbio.a dbio layer) and common math from libacqcommon.a.
*
* 복식부기 원칙: 모든 전표는 차변(D) 합 = 대변(C) 합. 계정잔액 balance = 차변-대변.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch libndrxxaecpg.so);
* there is NO EXEC SQL CONNECT. Each tpcall runs on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "lg_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void LG_POST(TPSVCINFO *p); void LG_CREDIT(TPSVCINFO *p);
void LG_DEBIT(TPSVCINFO *p); void LG_DOUBLE(TPSVCINFO *p);
void LG_BALCHK(TPSVCINFO *p); void LG_REVERSE(TPSVCINFO *p);
void LG_CARRY(TPSVCINFO *p); void LG_CLOSE_LINK(TPSVCINFO *p);
void LG_ACCTSUM(TPSVCINFO *p); void LG_RECALC(TPSVCINFO *p);
void LG_UNSETTLED(TPSVCINFO *p); void LG_STATS(TPSVCINFO *p);
void LG_DIFFCHK(TPSVCINFO *p); void LG_CORRECT(TPSVCINFO *p);
void LG_INQUIRY(TPSVCINFO *p); void LG_TRIALBAL(TPSVCINFO *p);
void LG_JOURNAL(TPSVCINFO *p); void LG_VOUCHER(TPSVCINFO *p);
void LG_APPLY(TPSVCINFO *p); void LG_ROLLFWD(TPSVCINFO *p);
void LG_MERCHBAL(TPSVCINFO *p); void LG_DAILYSUM(TPSVCINFO *p);
void LG_MONTHLYSUM(TPSVCINFO *p); void LG_ADJUST(TPSVCINFO *p);
void LG_TRANSFER(TPSVCINFO *p); void LG_SETTLEPOST(TPSVCINFO *p);
void LG_ACQPOST(TPSVCINFO *p); void LG_FEEPOST(TPSVCINFO *p);
void LG_STATUS(TPSVCINFO *p); void LG_CONFIRM(TPSVCINFO *p);
/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"LG_POST", LG_POST}, {"LG_CREDIT", LG_CREDIT},
{"LG_DEBIT", LG_DEBIT}, {"LG_DOUBLE", LG_DOUBLE},
{"LG_BALCHK", LG_BALCHK}, {"LG_REVERSE", LG_REVERSE},
{"LG_CARRY", LG_CARRY}, {"LG_CLOSE_LINK", LG_CLOSE_LINK},
{"LG_ACCTSUM", LG_ACCTSUM}, {"LG_RECALC", LG_RECALC},
{"LG_UNSETTLED", LG_UNSETTLED}, {"LG_STATS", LG_STATS},
{"LG_DIFFCHK", LG_DIFFCHK}, {"LG_CORRECT", LG_CORRECT},
{"LG_INQUIRY", LG_INQUIRY}, {"LG_TRIALBAL", LG_TRIALBAL},
{"LG_JOURNAL", LG_JOURNAL}, {"LG_VOUCHER", LG_VOUCHER},
{"LG_APPLY", LG_APPLY}, {"LG_ROLLFWD", LG_ROLLFWD},
{"LG_MERCHBAL", LG_MERCHBAL}, {"LG_DAILYSUM", LG_DAILYSUM},
{"LG_MONTHLYSUM", LG_MONTHLYSUM},{"LG_ADJUST", LG_ADJUST},
{"LG_TRANSFER", LG_TRANSFER}, {"LG_SETTLEPOST", LG_SETTLEPOST},
{"LG_ACQPOST", LG_ACQPOST}, {"LG_FEEPOST", LG_FEEPOST},
{"LG_STATUS", LG_STATUS}, {"LG_CONFIRM", LG_CONFIRM},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("lg_svr: tpopen FAIL: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("lg_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("lg_svr: %d lg 서비스 광고 완료, RM 개방", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("lg_svr: tpsvrdone");
}
/* ========================================================================= */
/* 1. LG_POST - 전표 작성(draft) + 차변/대변 균형 분개 2줄. */
/* ========================================================================= */
void LG_POST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char dacct[32], cacct[32], bizdate[16], merch[64];
long vid, ref, amount;
gets_(b, T_KEY1, dacct, sizeof(dacct)); if (dacct[0] == 0) strcpy(dacct, "RECV");
gets_(b, T_KEY2, cacct, sizeof(cacct)); if (cacct[0] == 0) strcpy(cacct, "MERCH_PAY");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
ref = getl(b, T_PURCHASE_ID);
amount = getl(b, T_AMOUNT);
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "POST", ref, "D", bizdate, "전표작성") < 0) FAIL(b);
if (lgdb_insert_line(vid, dacct, "D", amount, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, cacct, "C", amount, merch, bizdate, "posted") < 0) FAIL(b);
setl(b, T_ID1, vid);
Bchg(b, T_STATUS, 0, "D", 0L);
userlog("LG_POST vid=%ld 차변=%s 대변=%s 금액=%ld", vid, dacct, cacct, amount);
OK(b);
}
/* 2. LG_CREDIT - 기존 전표에 대변(C) 분개 1줄 추가. */
void LG_CREDIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], bizdate[16], merch[64];
long vid = getl(b, T_ID1), amount = getl(b, T_AMOUNT);
gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "MERCH_PAY");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
if (lgdb_insert_line(vid, acct, "C", amount, merch, bizdate, "posted") < 0) FAIL(b);
userlog("LG_CREDIT vid=%ld 대변 %s %ld", vid, acct, amount);
OK(b);
}
/* 3. LG_DEBIT - 기존 전표에 차변(D) 분개 1줄 추가. */
void LG_DEBIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], bizdate[16], merch[64];
long vid = getl(b, T_ID1), amount = getl(b, T_AMOUNT);
gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
if (lgdb_insert_line(vid, acct, "D", amount, merch, bizdate, "posted") < 0) FAIL(b);
userlog("LG_DEBIT vid=%ld 차변 %s %ld", vid, acct, amount);
OK(b);
}
/* 4. LG_DOUBLE - 신규 전표 즉시 전기(posted): 차변/대변 + 양 계정잔액 반영. */
void LG_DOUBLE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char dacct[32], cacct[32], bizdate[16], merch[64];
long vid, amount;
gets_(b, T_KEY1, dacct, sizeof(dacct)); if (dacct[0] == 0) strcpy(dacct, "RECV");
gets_(b, T_KEY2, cacct, sizeof(cacct)); if (cacct[0] == 0) strcpy(cacct, "MERCH_PAY");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
amount = getl(b, T_AMOUNT);
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "DOUBLE", getl(b, T_PURCHASE_ID), "P", bizdate, "복식전기") < 0) FAIL(b);
if (lgdb_insert_line(vid, dacct, "D", amount, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, cacct, "C", amount, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_upsert_acct_bal(dacct, bizdate, amount, 0) < 0) FAIL(b);
if (lgdb_upsert_acct_bal(cacct, bizdate, 0, amount) < 0) FAIL(b);
setl(b, T_ID1, vid);
Bchg(b, T_STATUS, 0, "P", 0L);
OK(b);
}
/* 5. LG_BALCHK - 전표 단위 대차 균형 점검 (차변합 vs 대변합). */
void LG_BALCHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_vid, h_dr, h_cr;
EXEC SQL END DECLARE SECTION;
h_vid = getl(b, T_ID1);
EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0)
INTO :h_dr, :h_cr FROM lg_ledger_line WHERE voucher_id = :h_vid;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_AMT2, h_dr);
setl(b, T_AMT3, h_cr);
setl(b, T_RC, (h_dr == h_cr) ? 0 : 1);
userlog("LG_BALCHK vid=%ld 차변=%ld 대변=%ld %s", h_vid, h_dr, h_cr,
(h_dr == h_cr) ? "균형" : "불균형");
OK(b);
}
/* 6. LG_REVERSE - 역분개 전표: 원전표 라인의 차/대를 뒤집어 신규 전표에 복사. */
void LG_REVERSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_src, h_new;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_src = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
h_new = lgdb_next_voucher_id();
if (h_new < 0) FAIL(b);
if (lgdb_insert_voucher(h_new, "REVERSE", h_src, "P", bizdate, "역분개") < 0) FAIL(b);
EXEC SQL INSERT INTO lg_ledger_line
(line_id, voucher_id, acct, dc, amount, merchant_id, biz_date, status)
SELECT nextval('lg_line_seq'), :h_new, acct,
CASE WHEN dc = 'D' THEN 'C' ELSE 'D' END,
amount, merchant_id, :h_bizdate, 'posted'
FROM lg_ledger_line WHERE voucher_id = :h_src;
if (sqlca.sqlcode < 0) { userlog("LG_REVERSE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
setl(b, T_ID2, h_new);
setl(b, T_COUNT, sqlca.sqlerrd[2]);
OK(b);
}
/* 7. LG_CARRY - 이월: 당일 계정잔액을 익영업일 개시잔액으로 이월(upsert). */
void LG_CARRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], nextday[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_next[16];
long h_cnt;
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (acq_next_bizday(bizdate, nextday) < 0) strcpy(nextday, bizdate);
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_next, nextday, sizeof(h_next)-1); h_next[sizeof(h_next)-1] = 0;
EXEC SQL INSERT INTO lg_account_bal (acct, biz_date, debit_total, credit_total, balance)
SELECT acct, :h_next, debit_total, credit_total, balance
FROM lg_account_bal WHERE biz_date = :h_bizdate
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = EXCLUDED.debit_total,
credit_total = EXCLUDED.credit_total,
balance = EXCLUDED.balance,
updated_at = now();
if (sqlca.sqlcode < 0) FAIL(b);
h_cnt = sqlca.sqlerrd[2];
setl(b, T_COUNT, h_cnt);
userlog("LG_CARRY %s -> %s 계정 %ld건 이월", bizdate, nextday, h_cnt);
OK(b);
}
/* 8. LG_CLOSE_LINK - 마감연계: 전표 라인 closed + 전표 상태 S(마감연계). */
void LG_CLOSE_LINK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_vid;
EXEC SQL END DECLARE SECTION;
h_vid = getl(b, T_ID1);
EXEC SQL UPDATE lg_ledger_line SET status = 'closed'
WHERE voucher_id = :h_vid AND status = 'posted';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, sqlca.sqlerrd[2]);
if (lgdb_update_voucher_status(h_vid, "S") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "S", 0L);
OK(b);
}
/* 9. LG_ACCTSUM - 계정 잔액 재집계(dbio) 후 잔액 반환. */
void LG_ACCTSUM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], bizdate[16];
long bal = 0;
gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (lgdb_recalc_acct_bal(acct, bizdate) < 0) FAIL(b);
if (lgdb_get_acct_bal(acct, bizdate, &bal) < 0) FAIL(b);
setl(b, T_NET, bal);
OK(b);
}
/* 10. LG_RECALC - 계정 잔액 원장 라인 기준 재계산(dbio). */
void LG_RECALC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], bizdate[16];
gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (lgdb_recalc_acct_bal(acct, bizdate) < 0) FAIL(b);
OK(b);
}
/* 11. LG_UNSETTLED - 미마감(status=posted) 원장 라인 건수. */
void LG_UNSETTLED(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM lg_ledger_line
WHERE biz_date = :h_bizdate AND status = 'posted';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 12. LG_STATS - 당일 전표/원장 통계(전표수, 총차변). */
void LG_STATS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_vc, h_dr;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(DISTINCT voucher_id),
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0)
INTO :h_vc, :h_dr FROM lg_ledger_line WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_vc);
setl(b, T_GROSS, h_dr);
OK(b);
}
/* 13. LG_DIFFCHK - 당일 전체 대차 차액 점검(총차변-총대변). */
void LG_DIFFCHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_dr, h_cr;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0)
INTO :h_dr, :h_cr FROM lg_ledger_line WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_AMT2, h_dr);
setl(b, T_AMT3, h_cr);
setl(b, T_DELTA, h_dr - h_cr);
setl(b, T_RC, (h_dr == h_cr) ? 0 : 1);
OK(b);
}
/* 14. LG_CORRECT - 원장 라인 금액 정정. */
void LG_CORRECT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_line, h_amount;
EXEC SQL END DECLARE SECTION;
h_line = getl(b, T_ID1);
h_amount = getl(b, T_AMOUNT);
EXEC SQL UPDATE lg_ledger_line SET amount = :h_amount WHERE line_id = :h_line;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
OK(b);
}
/* 15. LG_INQUIRY - 전표 상태 조회(dbio). */
void LG_INQUIRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long vid = getl(b, T_ID1);
char st[4];
if (lgdb_get_voucher_status(vid, st) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, st, 0L);
OK(b);
}
/* 16. LG_TRIALBAL - 계정별 시산표 산출 후 lg_trial_bal upsert. */
void LG_TRIALBAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_dr, h_cr;
char h_acct[32], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "RECV");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_acct, acct, sizeof(h_acct)-1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0)
INTO :h_dr, :h_cr FROM lg_ledger_line
WHERE acct = :h_acct AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO lg_trial_bal (acct, biz_date, debit_total, credit_total, diff)
VALUES (:h_acct, :h_bizdate, :h_dr, :h_cr, :h_dr - :h_cr)
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = EXCLUDED.debit_total, credit_total = EXCLUDED.credit_total,
diff = EXCLUDED.diff, updated_at = now();
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_AMT2, h_dr); setl(b, T_AMT3, h_cr); setl(b, T_DELTA, h_dr - h_cr);
OK(b);
}
/* 17. LG_JOURNAL - 당일 분개(원장 라인) 건수 조회. */
void LG_JOURNAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM lg_ledger_line WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 18. LG_VOUCHER - 빈 전표 헤더 생성(작성 상태). */
void LG_VOUCHER(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char vtype[16], bizdate[16], memo[128];
long vid, ref = getl(b, T_PURCHASE_ID);
gets_(b, T_STR2, vtype, sizeof(vtype)); if (vtype[0] == 0) strcpy(vtype, "POST");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MSG, memo, sizeof(memo)); if (memo[0] == 0) strcpy(memo, "전표생성");
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, vtype, ref, "D", bizdate, memo) < 0) FAIL(b);
setl(b, T_ID1, vid);
Bchg(b, T_STATUS, 0, "D", 0L);
OK(b);
}
/* 19. LG_APPLY - 전표 전기확정(P): 라인들의 계정잔액을 잔액테이블에 반영. */
void LG_APPLY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_vid;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_vid = getl(b, T_ID1);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO lg_account_bal (acct, biz_date, debit_total, credit_total, balance)
SELECT acct, :h_bizdate,
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE -amount END), 0)
FROM lg_ledger_line WHERE voucher_id = :h_vid
GROUP BY acct
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = lg_account_bal.debit_total + EXCLUDED.debit_total,
credit_total = lg_account_bal.credit_total + EXCLUDED.credit_total,
balance = lg_account_bal.balance + EXCLUDED.balance,
updated_at = now();
if (sqlca.sqlcode < 0) FAIL(b);
if (lgdb_update_voucher_status(h_vid, "P") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "P", 0L);
OK(b);
}
/* 20. LG_ROLLFWD - 전 계정 잔액 익일 이월(개시잔액 롤포워드). */
void LG_ROLLFWD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], nextday[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_next[16];
long h_cnt;
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (acq_next_bizday(bizdate, nextday) < 0) strcpy(nextday, bizdate);
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_next, nextday, sizeof(h_next)-1); h_next[sizeof(h_next)-1] = 0;
EXEC SQL INSERT INTO lg_account_bal (acct, biz_date, debit_total, credit_total, balance)
SELECT acct, :h_next, 0, 0, balance
FROM lg_account_bal WHERE biz_date = :h_bizdate
ON CONFLICT (acct, biz_date) DO UPDATE
SET balance = EXCLUDED.balance, updated_at = now();
if (sqlca.sqlcode < 0) FAIL(b);
h_cnt = sqlca.sqlerrd[2];
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 21. LG_MERCHBAL - 가맹점 잔액 재집계(dbio). */
void LG_MERCHBAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (merch[0] == 0) FAIL(b);
if (lgdb_upsert_merch_bal(merch, bizdate) < 0) FAIL(b);
OK(b);
}
/* 22. LG_DAILYSUM - 당일 계정별 잔액 일괄 재집계(전 계정 upsert). */
void LG_DAILYSUM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_cnt;
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO lg_account_bal (acct, biz_date, debit_total, credit_total, balance)
SELECT acct, :h_bizdate,
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE -amount END), 0)
FROM lg_ledger_line WHERE biz_date = :h_bizdate
GROUP BY acct
ON CONFLICT (acct, biz_date) DO UPDATE
SET debit_total = EXCLUDED.debit_total,
credit_total = EXCLUDED.credit_total,
balance = EXCLUDED.balance,
updated_at = now();
if (sqlca.sqlcode < 0) FAIL(b);
h_cnt = sqlca.sqlerrd[2];
setl(b, T_COUNT, h_cnt);
userlog("LG_DAILYSUM %s 계정 %ld건 집계", bizdate, h_cnt);
OK(b);
}
/* 23. LG_MONTHLYSUM - 월 계정과목별 차/대 집계 건수 조회(YYYY-MM). */
void LG_MONTHLYSUM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char month[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_dr;
char h_month[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR1, month, sizeof(month));
if (month[0] == 0) { gets_(b, T_BIZDATE, month, sizeof(month)); month[7] = 0; }
strncpy(h_month, month, sizeof(h_month)-1); h_month[sizeof(h_month)-1] = 0;
EXEC SQL SELECT count(DISTINCT acct),
coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0)
INTO :h_cnt, :h_dr FROM lg_ledger_line
WHERE to_char(biz_date, 'YYYY-MM') = :h_month;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_dr);
OK(b);
}
/* 24. LG_ADJUST - 조정 전표: 부호에 따라 차변/대변 단일 분개. */
void LG_ADJUST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], bizdate[16], merch[64];
long vid, delta, amt;
const char *dc;
gets_(b, T_KEY1, acct, sizeof(acct)); if (acct[0] == 0) strcpy(acct, "ADJ");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
delta = getl(b, T_DELTA);
dc = (delta >= 0) ? "D" : "C";
amt = (delta >= 0) ? delta : -delta;
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "ADJ", getl(b, T_PURCHASE_ID), "P", bizdate, "조정분개") < 0) FAIL(b);
if (lgdb_insert_line(vid, acct, dc, amt, merch, bizdate, "posted") < 0) FAIL(b);
setl(b, T_ID1, vid);
OK(b);
}
/* 25. LG_TRANSFER - 계정간 대체: 출금계정 대변, 입금계정 차변(균형). */
void LG_TRANSFER(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char src[32], dst[32], bizdate[16], merch[64];
long vid, amount;
gets_(b, T_KEY1, src, sizeof(src)); if (src[0] == 0) strcpy(src, "MERCH_PAY");
gets_(b, T_KEY2, dst, sizeof(dst)); if (dst[0] == 0) strcpy(dst, "CASH");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
amount = getl(b, T_AMOUNT);
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "TRANSFER", 0, "P", bizdate, "계정대체") < 0) FAIL(b);
if (lgdb_insert_line(vid, src, "C", amount, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, dst, "D", amount, merch, bizdate, "posted") < 0) FAIL(b);
setl(b, T_ID1, vid);
OK(b);
}
/* 26. LG_SETTLEPOST - 정산 전기: 차변 MERCH_PAY, 대변 CASH (net). */
void LG_SETTLEPOST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], merch[64];
long vid, net;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
net = getl(b, T_NET);
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "SETTLE", getl(b, T_PURCHASE_ID), "P", bizdate, "정산전기") < 0) FAIL(b);
if (lgdb_insert_line(vid, "MERCH_PAY", "D", net, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, "CASH", "C", net, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_upsert_merch_bal(merch, bizdate) < 0) FAIL(b);
setl(b, T_ID1, vid);
userlog("LG_SETTLEPOST vid=%ld merch=%s net=%ld", vid, merch, net);
OK(b);
}
/* 27. LG_ACQPOST - 매입 전기: 차변 RECV=amount, 대변 MERCH_PAY=net + FEE_INC=fee. */
void LG_ACQPOST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], merch[64];
long vid, amount, fee, net;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
amount = getl(b, T_AMOUNT);
fee = getl(b, T_FEE);
net = getl(b, T_NET);
if (net == 0) net = amount - fee;
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "ACQ", getl(b, T_PURCHASE_ID), "P", bizdate, "매입전기") < 0) FAIL(b);
if (lgdb_insert_line(vid, "RECV", "D", amount, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, "MERCH_PAY", "C", net, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, "FEE_INC", "C", fee, merch, bizdate, "posted") < 0) FAIL(b);
setl(b, T_ID1, vid);
userlog("LG_ACQPOST vid=%ld amount=%ld net=%ld fee=%ld", vid, amount, net, fee);
OK(b);
}
/* 28. LG_FEEPOST - 수수료수익 전기: 차변 RECV=fee, 대변 FEE_INC=fee. */
void LG_FEEPOST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], merch[64];
long vid, fee;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
fee = getl(b, T_FEE);
vid = lgdb_next_voucher_id();
if (vid < 0) FAIL(b);
if (lgdb_insert_voucher(vid, "FEE", getl(b, T_PURCHASE_ID), "P", bizdate, "수수료전기") < 0) FAIL(b);
if (lgdb_insert_line(vid, "RECV", "D", fee, merch, bizdate, "posted") < 0) FAIL(b);
if (lgdb_insert_line(vid, "FEE_INC", "C", fee, merch, bizdate, "posted") < 0) FAIL(b);
setl(b, T_ID1, vid);
OK(b);
}
/* 29. LG_STATUS - 전표 상태 직접 변경(setter). */
void LG_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char st[8];
long vid = getl(b, T_ID1);
gets_(b, T_STATUS, st, sizeof(st)); if (st[0] == 0) strcpy(st, "P");
if (lgdb_update_voucher_status(vid, st) < 0) FAIL(b);
OK(b);
}
/* 30. LG_CONFIRM - 전표 확정: 대차 균형이면 P로 확정, 아니면 실패. */
void LG_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_vid, h_dr, h_cr;
EXEC SQL END DECLARE SECTION;
h_vid = getl(b, T_ID1);
EXEC SQL SELECT coalesce(sum(CASE WHEN dc = 'D' THEN amount ELSE 0 END), 0),
coalesce(sum(CASE WHEN dc = 'C' THEN amount ELSE 0 END), 0)
INTO :h_dr, :h_cr FROM lg_ledger_line WHERE voucher_id = :h_vid;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_dr != h_cr) {
userlog("LG_CONFIRM vid=%ld 대차 불균형 차변=%ld 대변=%ld -> 거부", h_vid, h_dr, h_cr);
setl(b, T_RC, 1);
FAIL(b);
}
if (lgdb_update_voucher_status(h_vid, "P") < 0) FAIL(b);
setl(b, T_RC, 0);
Bchg(b, T_STATUS, 0, "P", 0L);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,56 @@
/*
* mg_resend_batch.pgc - mg 실패 전문 재전송 배치 (커서 + XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction, an EXEC SQL cursor sweeps mg_queue rows in F(실패)/Q(대기) state
* for a biz_date, re-queues each (status->Q, retry_cnt++) via the linked dbio
* lib, then tpcommit drives XA 2PC. Usage: mg_resend_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "mg_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_qid, h_retry;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE rsc CURSOR FOR
SELECT queue_id, retry_cnt FROM mg_queue
WHERE biz_date = :h_bizdate AND status = 'F'
ORDER BY queue_id;
EXEC SQL OPEN rsc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH rsc INTO :h_qid, :h_retry;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE rsc; tpabort(0); return 1; }
/* 실패 전문을 재전송 대기(Q)로 되돌리고 retry_cnt 증가 */
EXEC SQL UPDATE mg_queue
SET status = 'Q', retry_cnt = retry_cnt + 1, updated_at = now()
WHERE queue_id = :h_qid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { EXEC SQL CLOSE rsc; tpabort(0); return 1; }
rows++;
}
EXEC SQL CLOSE rsc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> mg_resend_batch COMMIT: bizdate=%s resent=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,62 @@
/*
* mg_stat_batch.pgc - mg 채널 일통계 배치 (커서 + XA).
*
* Cursors over mg_msg_log aggregated per channel for a biz_date and upserts
* mg_stat (수신/송신/오류 건수 + 금액합), all under ONE global XA transaction.
* Usage: mg_stat_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "mg_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
char h_chan[16];
long h_in, h_out, h_err, h_sum;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE msc CURSOR FOR
SELECT channel,
coalesce(sum(CASE WHEN direction='IN' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN direction='OUT' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN rc<>'00' THEN 1 ELSE 0 END),0),
coalesce(sum(amount),0)
FROM mg_msg_log WHERE biz_date = :h_bizdate
GROUP BY channel ORDER BY channel;
EXEC SQL OPEN msc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH msc INTO :h_chan, :h_in, :h_out, :h_err, :h_sum;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE msc; tpabort(0); return 1; }
EXEC SQL INSERT INTO mg_stat (channel, biz_date, in_cnt, out_cnt, err_cnt, amount_sum)
VALUES (:h_chan, :h_bizdate, :h_in, :h_out, :h_err, :h_sum)
ON CONFLICT (channel, biz_date) DO UPDATE
SET in_cnt = EXCLUDED.in_cnt, out_cnt = EXCLUDED.out_cnt,
err_cnt = EXCLUDED.err_cnt, amount_sum = EXCLUDED.amount_sum,
updated_at = now();
if (sqlca.sqlcode < 0) { fprintf(stderr, "UPSERT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE msc; tpabort(0); return 1; }
rows++;
}
EXEC SQL CLOSE msc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> mg_stat_batch COMMIT: bizdate=%s channels=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

41
app/src/mg/dbio/mg_dbio.h Normal file
View file

@ -0,0 +1,41 @@
/*
* mg_dbio.h - mg () DB (libmgdbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* mg_svr and the mg batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef MG_DBIO_H
#define MG_DBIO_H
/* 채번 (STAN/전문 시퀀스) */
long mgdb_next_stan(void);
long mgdb_next_msg_id(void);
long mgdb_next_queue_id(void);
long mgdb_seq_bump(const char *name); /* mg_seq 이름있는 카운터 +1 -> 새 값 */
/* 전문 로그 (mg_msg_log) */
int mgdb_insert_msg(long msg_id, long stan, const char *mti, const char *channel,
const char *direction, const char *proc_code, const char *bitmap,
long amount, const char *pan, const char *rc, const char *raw,
const char *status, const char *bizdate);
int mgdb_update_msg_rc(long msg_id, const char *rc, const char *status);
int mgdb_get_msg_status(long msg_id, char *out /* >= 2 bytes */);
long mgdb_find_msg_by_stan(long stan); /* msg_id 반환, 없으면 -1 */
/* 라우팅 (mg_route) */
int mgdb_insert_route(long route_id, const char *channel, const char *mti,
const char *dest, long priority, const char *bizdate);
int mgdb_lookup_route(const char *channel, const char *mti, char *dest_out /* >= 64 */);
/* 채널 (mg_channel) */
int mgdb_get_channel_status(const char *channel, char *status_out /* >= 16 */);
int mgdb_set_channel_status(const char *channel, const char *status);
int mgdb_set_channel_session(const char *channel, const char *session_key, long stan);
/* 큐 (mg_queue) */
int mgdb_enqueue(long queue_id, long stan, const char *channel, const char *mti,
const char *payload, const char *bizdate);
int mgdb_set_queue_status(long queue_id, const char *status);
#endif /* MG_DBIO_H */

View file

@ -0,0 +1,200 @@
/*
* mg_msg_dbio.pgc - 전문 로그/채번/큐 DB 접근 (part of libmgdbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "mg_dbio.h"
long mgdb_next_stan(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('mg_stan_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("mgdb_next_stan FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long mgdb_next_msg_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('mg_msg_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("mgdb_next_msg_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long mgdb_next_queue_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('mg_queue_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("mgdb_next_queue_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
/* mg_seq 이름있는 카운터를 원자적으로 +1 하고 새 값을 돌려준다 (세션 시퀀스 채번). */
long mgdb_seq_bump(const char *name)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_val;
char h_name[32];
EXEC SQL END DECLARE SECTION;
strncpy(h_name, name, sizeof(h_name) - 1); h_name[sizeof(h_name)-1] = 0;
EXEC SQL UPDATE mg_seq SET seq_val = seq_val + 1, updated_at = now()
WHERE seq_name = :h_name;
if (sqlca.sqlcode < 0) {
userlog("mgdb_seq_bump UPDATE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
if (sqlca.sqlcode == 100) { /* 최초 사용: 행 생성 */
EXEC SQL INSERT INTO mg_seq (seq_name, seq_val) VALUES (:h_name, 1);
if (sqlca.sqlcode < 0) return -1;
return 1;
}
EXEC SQL SELECT seq_val INTO :h_val FROM mg_seq WHERE seq_name = :h_name;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) return -1;
return h_val;
}
int mgdb_insert_msg(long msg_id, long stan, const char *mti, const char *channel,
const char *direction, const char *proc_code, const char *bitmap,
long amount, const char *pan, const char *rc, const char *raw,
const char *status, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = msg_id, h_stan = stan, h_amount = amount;
char h_mti[8], h_chan[16], h_dir[8], h_proc[8], h_bitmap[40];
char h_pan[24], h_rc[4], h_raw[512], h_status[4], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_mti, mti, sizeof(h_mti)-1); h_mti[sizeof(h_mti)-1] = 0;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_dir, direction, sizeof(h_dir)-1); h_dir[sizeof(h_dir)-1] = 0;
strncpy(h_proc, proc_code, sizeof(h_proc)-1); h_proc[sizeof(h_proc)-1] = 0;
strncpy(h_bitmap, bitmap, sizeof(h_bitmap)-1); h_bitmap[sizeof(h_bitmap)-1] = 0;
strncpy(h_pan, pan, sizeof(h_pan)-1); h_pan[sizeof(h_pan)-1] = 0;
strncpy(h_rc, rc, sizeof(h_rc)-1); h_rc[sizeof(h_rc)-1] = 0;
strncpy(h_raw, raw, sizeof(h_raw)-1); h_raw[sizeof(h_raw)-1] = 0;
strncpy(h_status, status, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO mg_msg_log
(msg_id, stan, mti, channel, direction, proc_code, bitmap, amount,
pan, rc, raw_msg, status, biz_date)
VALUES (:h_id, :h_stan, :h_mti, :h_chan, :h_dir, :h_proc, :h_bitmap,
:h_amount, :h_pan, :h_rc, :h_raw, :h_status, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("mgdb_insert_msg FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mgdb_update_msg_rc(long msg_id, const char *rc, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = msg_id;
char h_rc[4], h_status[4];
EXEC SQL END DECLARE SECTION;
strncpy(h_rc, rc, sizeof(h_rc)-1); h_rc[sizeof(h_rc)-1] = 0;
strncpy(h_status, status, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE mg_msg_log SET rc = :h_rc, status = :h_status WHERE msg_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mgdb_update_msg_rc FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mgdb_get_msg_status(long msg_id, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = msg_id;
char h_status[4];
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT status INTO :h_status FROM mg_msg_log WHERE msg_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mgdb_get_msg_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
out[0] = h_status[0];
out[1] = 0;
return 0;
}
long mgdb_find_msg_by_stan(long stan)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_stan = stan, h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT msg_id INTO :h_id FROM mg_msg_log
WHERE stan = :h_stan ORDER BY msg_id DESC LIMIT 1;
if (sqlca.sqlcode == 100) return -1;
if (sqlca.sqlcode < 0) {
userlog("mgdb_find_msg_by_stan FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int mgdb_enqueue(long queue_id, long stan, const char *channel, const char *mti,
const char *payload, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = queue_id, h_stan = stan;
char h_chan[16], h_mti[8], h_payload[512], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_mti, mti, sizeof(h_mti)-1); h_mti[sizeof(h_mti)-1] = 0;
strncpy(h_payload, payload, sizeof(h_payload)-1); h_payload[sizeof(h_payload)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO mg_queue (queue_id, stan, channel, mti, payload, status, biz_date)
VALUES (:h_id, :h_stan, :h_chan, :h_mti, :h_payload, 'Q', :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("mgdb_enqueue FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mgdb_set_queue_status(long queue_id, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = queue_id;
char h_status[4];
EXEC SQL END DECLARE SECTION;
strncpy(h_status, status, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE mg_queue SET status = :h_status, updated_at = now()
WHERE queue_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mgdb_set_queue_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

View file

@ -0,0 +1,111 @@
/*
* mg_route_dbio.pgc - 라우팅 룰 + 채널 세션 DB 접근 (part of libmgdbio.a).
*/
#include <string.h>
#include <userlog.h>
#include "mg_dbio.h"
int mgdb_insert_route(long route_id, const char *channel, const char *mti,
const char *dest, long priority, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = route_id, h_pri = priority;
char h_chan[16], h_mti[8], h_dest[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_mti, mti, sizeof(h_mti)-1); h_mti[sizeof(h_mti)-1] = 0;
strncpy(h_dest, dest, sizeof(h_dest)-1); h_dest[sizeof(h_dest)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO mg_route (route_id, channel, mti, dest, priority, active, biz_date)
VALUES (:h_id, :h_chan, :h_mti, :h_dest, :h_pri, true, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("mgdb_insert_route FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
/* 채널+전문유형으로 활성 라우팅 목적지를 우선순위 순으로 1건 조회. */
int mgdb_lookup_route(const char *channel, const char *mti, char *dest_out)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_chan[16], h_mti[8], h_dest[64];
EXEC SQL END DECLARE SECTION;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_mti, mti, sizeof(h_mti)-1); h_mti[sizeof(h_mti)-1] = 0;
EXEC SQL SELECT dest INTO :h_dest FROM mg_route
WHERE channel = :h_chan AND mti = :h_mti AND active = true
ORDER BY priority ASC LIMIT 1;
if (sqlca.sqlcode == 100) { dest_out[0] = 0; return -1; }
if (sqlca.sqlcode < 0) {
userlog("mgdb_lookup_route FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
strncpy(dest_out, h_dest, 63); dest_out[63] = 0;
return 0;
}
int mgdb_get_channel_status(const char *channel, char *status_out)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_chan[16], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
EXEC SQL SELECT status INTO :h_status FROM mg_channel WHERE channel = :h_chan;
if (sqlca.sqlcode == 100) { strcpy(status_out, "UNKNOWN"); return -1; }
if (sqlca.sqlcode < 0) {
userlog("mgdb_get_channel_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
strncpy(status_out, h_status, 15); status_out[15] = 0;
return 0;
}
int mgdb_set_channel_status(const char *channel, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_chan[16], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_status, status, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE mg_channel SET status = :h_status, updated_at = now()
WHERE channel = :h_chan;
if (sqlca.sqlcode < 0) {
userlog("mgdb_set_channel_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
if (sqlca.sqlcode == 100) { /* 신규 채널 등록 */
EXEC SQL INSERT INTO mg_channel (channel, status) VALUES (:h_chan, :h_status);
if (sqlca.sqlcode < 0) return -1;
}
return 0;
}
int mgdb_set_channel_session(const char *channel, const char *session_key, long stan)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_stan = stan;
char h_chan[16], h_key[64];
EXEC SQL END DECLARE SECTION;
strncpy(h_chan, channel, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_key, session_key, sizeof(h_key)-1); h_key[sizeof(h_key)-1] = 0;
EXEC SQL UPDATE mg_channel
SET session_key = :h_key, last_stan = :h_stan, status = 'SIGNON',
signon_at = now(), updated_at = now()
WHERE channel = :h_chan;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mgdb_set_channel_session FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

649
app/src/mg/mg_svr.pgc Normal file
View file

@ -0,0 +1,649 @@
/*
* mg_svr.pgc - mg (전문게이트웨이 / message gateway) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online mg services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct 전문
* (ISO8583) gateway operation with genuine EXEC SQL (directly, or via the linked
* libmgdbio.a dbio layer) plus 고정길이/비트맵 pack/unpack idioms and STAN 채번
* via a Postgres sequence.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch libndrxxaecpg.so);
* there is NO EXEC SQL CONNECT. Each tpcall runs on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "mg_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
static void sets_(UBFH *b, BFLDID f, const char *s) { Bchg(b, f, 0, (char *)s, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void MG_RECV(TPSVCINFO *p); void MG_SEND(TPSVCINFO *p);
void MG_ISO8583(TPSVCINFO *p); void MG_BITMAP(TPSVCINFO *p);
void MG_STAN(TPSVCINFO *p); void MG_ROUTE(TPSVCINFO *p);
void MG_RESEND(TPSVCINFO *p); void MG_CONVERT(TPSVCINFO *p);
void MG_ERRMSG(TPSVCINFO *p); void MG_LOG(TPSVCINFO *p);
void MG_CHANAUTH(TPSVCINFO *p); void MG_SESSION(TPSVCINFO *p);
void MG_ACK(TPSVCINFO *p); void MG_NACK(TPSVCINFO *p);
void MG_ECHO(TPSVCINFO *p); void MG_HEALTH(TPSVCINFO *p);
void MG_SIGNON(TPSVCINFO *p); void MG_SIGNOFF(TPSVCINFO *p);
void MG_KEYEXCH(TPSVCINFO *p); void MG_MAC(TPSVCINFO *p);
void MG_PARSE(TPSVCINFO *p); void MG_PACK(TPSVCINFO *p);
void MG_UNPACK(TPSVCINFO *p); void MG_QUEUE(TPSVCINFO *p);
void MG_DEQUEUE(TPSVCINFO *p); void MG_RETRY(TPSVCINFO *p);
void MG_STATS(TPSVCINFO *p); void MG_CHANSTAT(TPSVCINFO *p);
void MG_STATUS(TPSVCINFO *p); void MG_CONFIRM(TPSVCINFO *p);
/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"MG_RECV", MG_RECV}, {"MG_SEND", MG_SEND},
{"MG_ISO8583", MG_ISO8583}, {"MG_BITMAP", MG_BITMAP},
{"MG_STAN", MG_STAN}, {"MG_ROUTE", MG_ROUTE},
{"MG_RESEND", MG_RESEND}, {"MG_CONVERT", MG_CONVERT},
{"MG_ERRMSG", MG_ERRMSG}, {"MG_LOG", MG_LOG},
{"MG_CHANAUTH", MG_CHANAUTH}, {"MG_SESSION", MG_SESSION},
{"MG_ACK", MG_ACK}, {"MG_NACK", MG_NACK},
{"MG_ECHO", MG_ECHO}, {"MG_HEALTH", MG_HEALTH},
{"MG_SIGNON", MG_SIGNON}, {"MG_SIGNOFF", MG_SIGNOFF},
{"MG_KEYEXCH", MG_KEYEXCH}, {"MG_MAC", MG_MAC},
{"MG_PARSE", MG_PARSE}, {"MG_PACK", MG_PACK},
{"MG_UNPACK", MG_UNPACK}, {"MG_QUEUE", MG_QUEUE},
{"MG_DEQUEUE", MG_DEQUEUE}, {"MG_RETRY", MG_RETRY},
{"MG_STATS", MG_STATS}, {"MG_CHANSTAT", MG_CHANSTAT},
{"MG_STATUS", MG_STATUS}, {"MG_CONFIRM", MG_CONFIRM},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("mg_svr: tpopen FAIL: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("mg_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("mg_svr: %d mg 서비스 광고 완료, RM 오픈", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("mg_svr: tpsvrdone");
}
/* ----- module-local pure helpers (고정길이/비트맵/체크섬) ----------------- */
/* 16-hex primary bitmap from a 64-bit field-present mask. */
static void mg_bitmap_hex(unsigned long mask, char *out /* >= 17 */)
{
sprintf(out, "%016lX", mask);
}
/* simple additive 4-hex MAC/checksum over a buffer (전문 무결성 데모). */
static void mg_checksum(const char *s, char *out /* >= 5 */)
{
unsigned int sum = 0; int i;
for (i = 0; s[i]; i++) sum = (sum + (unsigned char)s[i] * 31u) & 0xFFFF;
sprintf(out, "%04X", sum);
}
/* ========================================================================= */
/* 1. MG_RECV - 전문 수신 접수 (direction=IN). STAN 채번 후 로그 적재. */
/* ========================================================================= */
void MG_RECV(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], raw[512], bizdate[16], pan[24];
long stan, msg_id, amount;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_STR5, pan, sizeof(pan));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
amount = getl(b, T_AMOUNT);
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
msg_id = mgdb_next_msg_id(); if (msg_id < 0) FAIL(b);
if (mgdb_insert_msg(msg_id, stan, mti, chan, "IN", "000000", "", amount,
pan, "00", raw, "R", bizdate) < 0) FAIL(b);
setl(b, T_ARG1, stan);
setl(b, T_ID1, msg_id);
userlog("MG_RECV 수신 stan=%ld msg_id=%ld chan=%s mti=%s amt=%ld", stan, msg_id, chan, mti, amount);
OK(b);
}
/* 2. MG_SEND - 전문 송신 (direction=OUT). */
void MG_SEND(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], raw[512], bizdate[16];
long stan, msg_id, amount;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0210");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
amount = getl(b, T_AMOUNT);
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
msg_id = mgdb_next_msg_id(); if (msg_id < 0) FAIL(b);
if (mgdb_insert_msg(msg_id, stan, mti, chan, "OUT", "000000", "", amount,
"", "00", raw, "S", bizdate) < 0) FAIL(b);
setl(b, T_ARG1, stan);
setl(b, T_ID1, msg_id);
OK(b);
}
/* 3. MG_ISO8583 - ISO8583 전문 파싱 후 로그 (mti=전문 앞 4자리). */
void MG_ISO8583(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], raw[512], bizdate[16], mti[8], proc[8];
long stan, msg_id, amount, n;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
n = (long)strlen(raw);
/* 고정길이 전문: [0..3]=MTI, [4..9]=처리코드, [10..21]=금액(12자리) */
memset(mti, 0, sizeof(mti)); memset(proc, 0, sizeof(proc));
if (n >= 4) strncpy(mti, raw, 4);
else strcpy(mti, "0200");
if (n >= 10) strncpy(proc, raw + 4, 6);
else strcpy(proc, "000000");
amount = (n >= 22) ? atol(raw + 10) : getl(b, T_AMOUNT);
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
msg_id = mgdb_next_msg_id(); if (msg_id < 0) FAIL(b);
if (mgdb_insert_msg(msg_id, stan, mti, chan, "IN", proc, "", amount,
"", "00", raw, "R", bizdate) < 0) FAIL(b);
sets_(b, T_STR1, mti);
sets_(b, T_STR4, proc);
setl(b, T_AMOUNT, amount);
setl(b, T_ID1, msg_id);
OK(b);
}
/* 4. MG_BITMAP - 비트맵 계산 후 해당 전문(msg_id)에 반영. */
void MG_BITMAP(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char hex[24];
unsigned long mask = (unsigned long)getl(b, T_ARG1); /* 필드 존재 마스크 */
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
char h_bitmap[40];
EXEC SQL END DECLARE SECTION;
mg_bitmap_hex(mask, hex);
sets_(b, T_STR6, hex);
h_id = getl(b, T_ID1);
if (h_id > 0) {
strncpy(h_bitmap, hex, sizeof(h_bitmap)-1); h_bitmap[sizeof(h_bitmap)-1] = 0;
EXEC SQL UPDATE mg_msg_log SET bitmap = :h_bitmap WHERE msg_id = :h_id;
if (sqlca.sqlcode < 0) { userlog("MG_BITMAP UPDATE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
}
OK(b);
}
/* 5. MG_STAN - STAN 채번 (Postgres sequence). */
void MG_STAN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long stan = mgdb_next_stan();
if (stan < 0) FAIL(b);
setl(b, T_ARG1, stan);
OK(b);
}
/* 6. MG_ROUTE - 채널+전문유형 -> 목적지 라우팅 조회. */
void MG_ROUTE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], dest[64];
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200");
if (mgdb_lookup_route(chan, mti, dest) < 0) {
userlog("MG_ROUTE 라우팅 없음 chan=%s mti=%s", chan, mti);
setl(b, T_RC, 1);
FAIL(b);
}
sets_(b, T_STR3, dest);
setl(b, T_RC, 0);
OK(b);
}
/* 7. MG_RESEND - 전문 재전송: STAN 으로 원전문 찾아 큐에 적재. */
void MG_RESEND(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], raw[512], bizdate[16];
long stan, msg_id, qid;
stan = getl(b, T_ARG1);
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
msg_id = mgdb_find_msg_by_stan(stan);
if (msg_id < 0) { userlog("MG_RESEND 원전문 없음 stan=%ld", stan); FAIL(b); }
qid = mgdb_next_queue_id(); if (qid < 0) FAIL(b);
if (mgdb_enqueue(qid, stan, chan, mti, raw, bizdate) < 0) FAIL(b);
setl(b, T_ID2, qid);
userlog("MG_RESEND 재전송 큐적재 stan=%ld qid=%ld", stan, qid);
OK(b);
}
/* 8. MG_CONVERT - 요청전문 -> 응답전문 변환 (MTI +0x10) 후 송신 로그. */
void MG_CONVERT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], rmti[8], raw[512], bizdate[16];
long stan, msg_id, amount, mnum;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
amount = getl(b, T_AMOUNT);
mnum = atol(mti) + 10; /* 0200 -> 0210 응답 */
sprintf(rmti, "%04ld", mnum);
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
msg_id = mgdb_next_msg_id(); if (msg_id < 0) FAIL(b);
if (mgdb_insert_msg(msg_id, stan, rmti, chan, "OUT", "000000", "", amount,
"", "00", raw, "S", bizdate) < 0) FAIL(b);
sets_(b, T_STR1, rmti);
setl(b, T_ID1, msg_id);
OK(b);
}
/* 9. MG_ERRMSG - 오류 전문 기록 (status=N, rc=입력). */
void MG_ERRMSG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], raw[512], bizdate[16], rc[4];
long stan, msg_id, rcnum;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0210");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
rcnum = getl(b, T_RC); if (rcnum <= 0) rcnum = 96;
sprintf(rc, "%02ld", rcnum % 100);
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
msg_id = mgdb_next_msg_id(); if (msg_id < 0) FAIL(b);
if (mgdb_insert_msg(msg_id, stan, mti, chan, "OUT", "000000", "", 0,
"", rc, raw, "N", bizdate) < 0) FAIL(b);
setl(b, T_ID1, msg_id);
OK(b);
}
/* 10. MG_LOG - 채널 당일 전문 로그 건수 조회. */
void MG_LOG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_chan[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_chan, chan, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM mg_msg_log
WHERE channel = :h_chan AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 11. MG_CHANAUTH - 채널 인증: UP/SIGNON 상태만 통과. */
void MG_CHANAUTH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], st[16];
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
if (mgdb_get_channel_status(chan, st) < 0) { setl(b, T_RC, 2); FAIL(b); }
if (strcmp(st, "UP") == 0 || strcmp(st, "SIGNON") == 0) {
setl(b, T_RC, 0);
sets_(b, T_STATUS, st);
OK(b);
}
userlog("MG_CHANAUTH 거부 chan=%s status=%s", chan, st);
setl(b, T_RC, 1);
sets_(b, T_STATUS, st);
FAIL(b);
}
/* 12. MG_SESSION - 세션 시퀀스 채번 (mg_seq SESSION). */
void MG_SESSION(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long v = mgdb_seq_bump("SESSION");
if (v < 0) FAIL(b);
setl(b, T_ARG2, v);
OK(b);
}
/* 13. MG_ACK - 응답 확인 (status=A, rc=00). */
void MG_ACK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long msg_id = getl(b, T_ID1);
if (mgdb_update_msg_rc(msg_id, "00", "A") < 0) FAIL(b);
sets_(b, T_STATUS, "A");
OK(b);
}
/* 14. MG_NACK - 부정 응답 (status=N, rc=입력). */
void MG_NACK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char rc[4];
long msg_id = getl(b, T_ID1), rcnum = getl(b, T_RC);
if (rcnum <= 0) rcnum = 91;
sprintf(rc, "%02ld", rcnum % 100);
if (mgdb_update_msg_rc(msg_id, rc, "N") < 0) FAIL(b);
sets_(b, T_STATUS, "N");
OK(b);
}
/* 15. MG_ECHO - 에코 전문(0800) 처리 + 채널 UP 표시. */
void MG_ECHO(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], bizdate[16];
long stan, msg_id;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
msg_id = mgdb_next_msg_id(); if (msg_id < 0) FAIL(b);
if (mgdb_insert_msg(msg_id, stan, "0800", chan, "IN", "990000", "", 0,
"", "00", "ECHO-TEST", "A", bizdate) < 0) FAIL(b);
if (mgdb_set_channel_status(chan, "UP") < 0) FAIL(b);
setl(b, T_ID1, msg_id);
sets_(b, T_STATUS, "UP");
OK(b);
}
/* 16. MG_HEALTH - 게이트웨이 헬스: 활성 채널 수 조회. */
void MG_HEALTH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_up;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT count(*) INTO :h_up FROM mg_channel WHERE status IN ('UP','SIGNON');
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_up);
setl(b, T_RC, 0);
OK(b);
}
/* 17. MG_SIGNON - 채널 사인온 + 세션키 설정. */
void MG_SIGNON(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], key[64];
long stan;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_KEY1, key, sizeof(key)); if (key[0] == 0) strcpy(key, "SESSION-KEY-DFLT");
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
if (mgdb_set_channel_session(chan, key, stan) < 0) FAIL(b);
sets_(b, T_STATUS, "SIGNON");
userlog("MG_SIGNON chan=%s stan=%ld", chan, stan);
OK(b);
}
/* 18. MG_SIGNOFF - 채널 사인오프 (status=DOWN). */
void MG_SIGNOFF(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16];
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
if (mgdb_set_channel_status(chan, "DOWN") < 0) FAIL(b);
sets_(b, T_STATUS, "DOWN");
OK(b);
}
/* 19. MG_KEYEXCH - 키 교환: 키버전 채번 + 세션키 갱신. */
void MG_KEYEXCH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], key[64];
long ver, stan;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_KEY1, key, sizeof(key)); if (key[0] == 0) strcpy(key, "NEW-KEY");
ver = mgdb_seq_bump("KEYVER"); if (ver < 0) FAIL(b);
stan = mgdb_next_stan(); if (stan < 0) FAIL(b);
if (mgdb_set_channel_session(chan, key, stan) < 0) FAIL(b);
setl(b, T_ARG2, ver);
userlog("MG_KEYEXCH chan=%s keyver=%ld", chan, ver);
OK(b);
}
/* 20. MG_MAC - 전문 MAC 계산 후 해당 전문에 반영 (무결성). */
void MG_MAC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char raw[512], mac[8];
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
char h_mac[8];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR3, raw, sizeof(raw));
mg_checksum(raw, mac);
sets_(b, T_KEY1, mac);
h_id = getl(b, T_ID1);
if (h_id > 0) {
strncpy(h_mac, mac, sizeof(h_mac)-1); h_mac[sizeof(h_mac)-1] = 0;
EXEC SQL UPDATE mg_msg_log SET mac = :h_mac WHERE msg_id = :h_id;
if (sqlca.sqlcode < 0) { userlog("MG_MAC UPDATE FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
}
OK(b);
}
/* 21. MG_PARSE - 고정길이 전문에서 처리코드 필드 추출 (순수 계산). */
void MG_PARSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char raw[512], mti[8], proc[8];
long n;
gets_(b, T_STR3, raw, sizeof(raw));
n = (long)strlen(raw);
memset(mti, 0, sizeof(mti)); memset(proc, 0, sizeof(proc));
if (n >= 4) strncpy(mti, raw, 4); else strcpy(mti, "0000");
if (n >= 10) strncpy(proc, raw + 4, 6); else strcpy(proc, "000000");
sets_(b, T_STR1, mti);
sets_(b, T_STR4, proc);
setl(b, T_COUNT, n);
OK(b);
}
/* 22. MG_PACK - 고정길이 전문 조립 (순수 계산): MTI(4)+처리코드(6)+금액(12). */
void MG_PACK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char mti[8], proc[8], raw[64];
long amount;
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200");
gets_(b, T_STR4, proc, sizeof(proc)); if (proc[0] == 0) strcpy(proc, "000000");
amount = getl(b, T_AMOUNT);
sprintf(raw, "%.4s%.6s%012ld", mti, proc, amount);
sets_(b, T_STR3, raw);
setl(b, T_COUNT, (long)strlen(raw));
OK(b);
}
/* 23. MG_UNPACK - 고정길이 전문 해체 (순수 계산). */
void MG_UNPACK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char raw[512], mti[8], proc[8];
long amount, n;
gets_(b, T_STR3, raw, sizeof(raw));
n = (long)strlen(raw);
memset(mti, 0, sizeof(mti)); memset(proc, 0, sizeof(proc));
if (n >= 4) strncpy(mti, raw, 4); else strcpy(mti, "0000");
if (n >= 10) strncpy(proc, raw + 4, 6); else strcpy(proc, "000000");
amount = (n >= 22) ? atol(raw + 10) : 0;
sets_(b, T_STR1, mti);
sets_(b, T_STR4, proc);
setl(b, T_AMOUNT, amount);
OK(b);
}
/* 24. MG_QUEUE - 송신 큐 적재. */
void MG_QUEUE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], mti[8], raw[512], bizdate[16];
long stan, qid;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_STR1, mti, sizeof(mti)); if (mti[0] == 0) strcpy(mti, "0200");
gets_(b, T_STR3, raw, sizeof(raw));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
stan = getl(b, T_ARG1);
if (stan <= 0) { stan = mgdb_next_stan(); if (stan < 0) FAIL(b); }
qid = mgdb_next_queue_id(); if (qid < 0) FAIL(b);
if (mgdb_enqueue(qid, stan, chan, mti, raw, bizdate) < 0) FAIL(b);
setl(b, T_ID2, qid);
setl(b, T_ARG1, stan);
OK(b);
}
/* 25. MG_DEQUEUE - 대기(Q) 전문 1건 인출 후 송신중(S) 전이. */
void MG_DEQUEUE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_qid, h_stan;
char h_payload[512], h_chan[16];
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT queue_id, stan, channel, payload INTO :h_qid, :h_stan, :h_chan, :h_payload
FROM mg_queue WHERE status = 'Q' ORDER BY queue_id ASC LIMIT 1;
if (sqlca.sqlcode == 100) { setl(b, T_COUNT, 0); OK(b); }
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL UPDATE mg_queue SET status = 'S', updated_at = now() WHERE queue_id = :h_qid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_ID2, h_qid);
setl(b, T_ARG1, h_stan);
sets_(b, T_CHANNEL, h_chan);
sets_(b, T_STR3, h_payload);
setl(b, T_COUNT, 1);
OK(b);
}
/* 26. MG_RETRY - 큐 재시도: retry_cnt +1, status=Q. */
void MG_RETRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_qid, h_retry;
EXEC SQL END DECLARE SECTION;
h_qid = getl(b, T_ID2);
EXEC SQL UPDATE mg_queue SET retry_cnt = retry_cnt + 1, status = 'Q', updated_at = now()
WHERE queue_id = :h_qid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
EXEC SQL SELECT retry_cnt INTO :h_retry FROM mg_queue WHERE queue_id = :h_qid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_COUNT, h_retry);
OK(b);
}
/* 27. MG_STATS - 채널 당일 통계: 수신/송신/오류 건수. */
void MG_STATS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_in, h_out, h_err, h_sum;
char h_chan[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_chan, chan, sizeof(h_chan)-1); h_chan[sizeof(h_chan)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT
coalesce(sum(CASE WHEN direction='IN' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN direction='OUT' THEN 1 ELSE 0 END),0),
coalesce(sum(CASE WHEN rc<>'00' THEN 1 ELSE 0 END),0),
coalesce(sum(amount),0)
INTO :h_in, :h_out, :h_err, :h_sum
FROM mg_msg_log WHERE channel = :h_chan AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_ARG1, h_in);
setl(b, T_ARG2, h_out);
setl(b, T_ARG3, h_err);
setl(b, T_GROSS, h_sum);
OK(b);
}
/* 28. MG_CHANSTAT - 채널 상태 조회. */
void MG_CHANSTAT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char chan[16], st[16];
gets_(b, T_CHANNEL, chan, sizeof(chan)); if (chan[0] == 0) strcpy(chan, "CH01");
if (mgdb_get_channel_status(chan, st) < 0) FAIL(b);
sets_(b, T_STATUS, st);
OK(b);
}
/* 29. MG_STATUS - 전문 처리 상태 조회 (msg_id). */
void MG_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long msg_id = getl(b, T_ID1);
char st[4];
if (mgdb_get_msg_status(msg_id, st) < 0) FAIL(b);
sets_(b, T_STATUS, st);
OK(b);
}
/* 30. MG_CONFIRM - 전문 확정 (status=C). */
void MG_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long msg_id = getl(b, T_ID1);
if (mgdb_update_msg_rc(msg_id, "00", "C") < 0) FAIL(b);
sets_(b, T_STATUS, "C");
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,68 @@
/*
* mm_grade_recalc_batch.pgc - 가맹점 등급 재산정 배치 (누적매출 커서 + XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction, an EXEC SQL cursor sums each merchant's 누적 매출(gross) from
* merchant_summary, derives a grade (S/A/B/C) and UPDATEs mm_merchant_ext (via
* the linked dbio lib), then tpcommit drives XA 2PC.
* Usage: mm_grade_recalc_batch
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "mm_dbio.h"
/* 누적매출 -> 등급 임계 (원): S>=10억, A>=3억, B>=5천만, 그 외 C */
static const char *grade_of(long gross)
{
if (gross >= 1000000000L) return "S";
if (gross >= 300000000L) return "A";
if (gross >= 50000000L) return "B";
return "C";
}
int main(int argc, char **argv)
{
long rows = 0;
(void)argc; (void)argv;
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64];
long h_gross;
EXEC SQL END DECLARE SECTION;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
/* 가맹점별 누적 매출 = merchant_summary.gross_amount 합계 */
EXEC SQL DECLARE grc CURSOR FOR
SELECT ms.merchant_id, coalesce(sum(ms.gross_amount),0)
FROM merchant_summary ms
JOIN mm_merchant_ext me ON me.merchant_id = ms.merchant_id
WHERE me.status = 'ACTIVE'
GROUP BY ms.merchant_id ORDER BY ms.merchant_id;
EXEC SQL OPEN grc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH grc INTO :h_merch, :h_gross;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE grc; tpabort(0); return 1; }
if (mmdb_update_merchant_grade(h_merch, grade_of(h_gross)) < 0) {
EXEC SQL CLOSE grc; tpabort(0); return 1;
}
if (mmdb_insert_merch_hist(h_merch, "GRADE", "", grade_of(h_gross), "배치 재산정") < 0) {
EXEC SQL CLOSE grc; tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE grc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> mm_grade_recalc_batch COMMIT: 재산정 가맹점=%ld\n", rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,53 @@
/*
* mm_stat_batch.pgc - 등급별 가맹점 수 통계 배치 (커서 + XA).
*
* Cursors over mm_merchant_ext grouped by grade, upserting a per-grade count
* into mm_grade_stat for the run date, all under ONE global XA transaction.
* Usage: mm_stat_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *statdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_statdate[16], h_grade[8];
long h_cnt;
EXEC SQL END DECLARE SECTION;
strncpy(h_statdate, statdate, sizeof(h_statdate)-1); h_statdate[sizeof(h_statdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE gsc CURSOR FOR
SELECT grade, count(*) FROM mm_merchant_ext
WHERE status = 'ACTIVE' GROUP BY grade ORDER BY grade;
EXEC SQL OPEN gsc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH gsc INTO :h_grade, :h_cnt;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE gsc; tpabort(0); return 1; }
EXEC SQL INSERT INTO mm_grade_stat (stat_date, grade, merch_cnt)
VALUES (:h_statdate, :h_grade, :h_cnt)
ON CONFLICT (stat_date, grade) DO UPDATE
SET merch_cnt = EXCLUDED.merch_cnt, updated_at = now();
if (sqlca.sqlcode < 0) { fprintf(stderr, "UPSERT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE gsc; tpabort(0); return 1; }
rows++;
}
EXEC SQL CLOSE gsc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> mm_stat_batch COMMIT: statdate=%s 등급구간=%ld\n", statdate, rows);
tpclose(); tpterm();
return 0;
}

28
app/src/mm/dbio/mm_dbio.h Normal file
View file

@ -0,0 +1,28 @@
/*
* mm_dbio.h - mm (/master) DB (libmmdbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* mm_svr and the mm batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef MM_DBIO_H
#define MM_DBIO_H
/* 가맹점 마스터 (mm_merch_dbio) */
long mmdb_next_hist_id(void);
int mmdb_insert_merchant(const char *merch, const char *name, const char *grade,
const char *acct, const char *status);
int mmdb_get_merchant(const char *merch, char *name_out /*>=64*/,
char *grade_out /*>=8*/, char *status_out /*>=16*/);
int mmdb_update_merchant_status(const char *merch, const char *status);
int mmdb_update_merchant_grade(const char *merch, const char *grade);
int mmdb_insert_merch_hist(const char *merch, const char *action,
const char *before_val, const char *after_val,
const char *memo);
/* 수수료율 마스터 (mm_feerate_dbio) */
int mmdb_set_fee_rate(const char *merch, const char *card_type, long rate_bps);
int mmdb_get_fee_rate(const char *merch, const char *card_type, long *bps_out);
long mmdb_feerate_hist_count(const char *merch);
#endif /* MM_DBIO_H */

View file

@ -0,0 +1,77 @@
/*
* mm_feerate_dbio.pgc - 수수료율 마스터 upsert + 이력 적재 (libmmdbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "mm_dbio.h"
int mmdb_set_fee_rate(const char *merch, const char *card_type, long rate_bps)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_bps = rate_bps;
char h_merch[64], h_ctype[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_ctype, card_type, sizeof(h_ctype) - 1); h_ctype[sizeof(h_ctype)-1] = 0;
EXEC SQL INSERT INTO mm_fee_rate (merchant_id, card_type, rate_bps, eff_date)
VALUES (:h_merch, :h_ctype, :h_bps, current_date)
ON CONFLICT (merchant_id, card_type) DO UPDATE
SET rate_bps = EXCLUDED.rate_bps, eff_date = current_date, updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("mmdb_set_fee_rate FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
/* 이력 적재 */
EXEC SQL SELECT nextval('mm_feerate_hist_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO mm_fee_rate_hist
(hist_id, merchant_id, card_type, rate_bps, eff_date)
VALUES (:h_id, :h_merch, :h_ctype, :h_bps, current_date);
if (sqlca.sqlcode < 0) {
userlog("mmdb_set_fee_rate hist FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mmdb_get_fee_rate(const char *merch, const char *card_type, long *bps_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_bps;
char h_merch[64], h_ctype[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_ctype, card_type, sizeof(h_ctype) - 1); h_ctype[sizeof(h_ctype)-1] = 0;
EXEC SQL SELECT rate_bps INTO :h_bps FROM mm_fee_rate
WHERE merchant_id = :h_merch AND card_type = :h_ctype;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mmdb_get_fee_rate FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
*bps_out = h_bps;
return 0;
}
long mmdb_feerate_hist_count(const char *merch)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM mm_fee_rate_hist
WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0) {
userlog("mmdb_feerate_hist_count FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_cnt;
}

View file

@ -0,0 +1,133 @@
/*
* mm_merch_dbio.pgc - 가맹점 확장 마스터 CRUD + 변경 이력 (libmmdbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "mm_dbio.h"
long mmdb_next_hist_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('mm_merch_hist_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("mmdb_next_hist_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int mmdb_insert_merchant(const char *merch, const char *name, const char *grade,
const char *acct, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_name[128], h_grade[8], h_acct[64], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_name, name, sizeof(h_name) - 1); h_name[sizeof(h_name)-1] = 0;
strncpy(h_grade, grade, sizeof(h_grade) - 1); h_grade[sizeof(h_grade)-1] = 0;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1]= 0;
EXEC SQL INSERT INTO mm_merchant_ext
(merchant_id, name, grade, settle_acct, status)
VALUES (:h_merch, :h_name, :h_grade, :h_acct, :h_status)
ON CONFLICT (merchant_id) DO UPDATE
SET name = EXCLUDED.name, grade = EXCLUDED.grade,
settle_acct = EXCLUDED.settle_acct, status = EXCLUDED.status,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("mmdb_insert_merchant FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mmdb_get_merchant(const char *merch, char *name_out, char *grade_out,
char *status_out)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_name[128], h_grade[8], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT name, grade, status INTO :h_name, :h_grade, :h_status
FROM mm_merchant_ext WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mmdb_get_merchant FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
strncpy(name_out, h_name, 63); name_out[63] = 0;
strncpy(grade_out, h_grade, 7); grade_out[7] = 0;
strncpy(status_out, h_status, 15); status_out[15] = 0;
return 0;
}
int mmdb_update_merchant_status(const char *merch, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1]= 0;
EXEC SQL UPDATE mm_merchant_ext SET status = :h_status, updated_at = now()
WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mmdb_update_merchant_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mmdb_update_merchant_grade(const char *merch, const char *grade)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_grade[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_grade, grade, sizeof(h_grade) - 1); h_grade[sizeof(h_grade)-1] = 0;
EXEC SQL UPDATE mm_merchant_ext SET grade = :h_grade, updated_at = now()
WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("mmdb_update_merchant_grade FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int mmdb_insert_merch_hist(const char *merch, const char *action,
const char *before_val, const char *after_val,
const char *memo)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
char h_merch[64], h_action[16], h_before[64], h_after[64], h_memo[128];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_action, action, sizeof(h_action) - 1); h_action[sizeof(h_action)-1]= 0;
strncpy(h_before, before_val, sizeof(h_before) - 1); h_before[sizeof(h_before)-1]= 0;
strncpy(h_after, after_val, sizeof(h_after) - 1); h_after[sizeof(h_after)-1] = 0;
strncpy(h_memo, memo, sizeof(h_memo) - 1); h_memo[sizeof(h_memo)-1] = 0;
EXEC SQL SELECT nextval('mm_merch_hist_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO mm_merch_hist
(hist_id, merchant_id, action, before_val, after_val, memo)
VALUES (:h_id, :h_merch, :h_action, :h_before, :h_after, :h_memo);
if (sqlca.sqlcode < 0) {
userlog("mmdb_insert_merch_hist FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

655
app/src/mm/mm_svr.pgc Normal file
View file

@ -0,0 +1,655 @@
/*
* mm_svr.pgc - mm (마스터/master) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online mm services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct
* 마스터(기준정보) operation with genuine EXEC SQL (directly, or via the linked
* libmmdbio.a dbio layer) and common math from libacqcommon.a.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch libndrxxaecpg.so);
* there is NO EXEC SQL CONNECT. Each tpcall runs on this process's XA branch, so
* a service sees its OWN uncommitted writes but NOT a sibling branch's.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "mm_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void MM_MERCH_REG(TPSVCINFO *p); void MM_MERCH_UPD(TPSVCINFO *p);
void MM_MERCH_INQ(TPSVCINFO *p); void MM_MERCH_DEL(TPSVCINFO *p);
void MM_FEERATE_SET(TPSVCINFO *p); void MM_FEERATE_INQ(TPSVCINFO *p);
void MM_FEERATE_HIST(TPSVCINFO *p); void MM_BIN_REG(TPSVCINFO *p);
void MM_BIN_INQ(TPSVCINFO *p); void MM_LIMIT_SET(TPSVCINFO *p);
void MM_LIMIT_INQ(TPSVCINFO *p); void MM_CODE_REG(TPSVCINFO *p);
void MM_CODE_INQ(TPSVCINFO *p); void MM_ISSUER_REG(TPSVCINFO *p);
void MM_ISSUER_INQ(TPSVCINFO *p); void MM_MERCH_STATUS(TPSVCINFO *p);
void MM_MERCH_ACCT(TPSVCINFO *p); void MM_MERCH_GRADE(TPSVCINFO *p);
void MM_TERMINAL_REG(TPSVCINFO *p); void MM_TERMINAL_INQ(TPSVCINFO *p);
void MM_MCC_REG(TPSVCINFO *p); void MM_HOLIDAY_REG(TPSVCINFO *p);
void MM_FX_REG(TPSVCINFO *p); void MM_FX_INQ(TPSVCINFO *p);
void MM_MERCH_BULK(TPSVCINFO *p); void MM_VALIDATE(TPSVCINFO *p);
void MM_MERCH_SUM(TPSVCINFO *p); void MM_CODE_CACHE(TPSVCINFO *p);
void MM_STATUS(TPSVCINFO *p); void MM_CONFIRM(TPSVCINFO *p);
/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"MM_MERCH_REG", MM_MERCH_REG}, {"MM_MERCH_UPD", MM_MERCH_UPD},
{"MM_MERCH_INQ", MM_MERCH_INQ}, {"MM_MERCH_DEL", MM_MERCH_DEL},
{"MM_FEERATE_SET", MM_FEERATE_SET}, {"MM_FEERATE_INQ", MM_FEERATE_INQ},
{"MM_FEERATE_HIST", MM_FEERATE_HIST}, {"MM_BIN_REG", MM_BIN_REG},
{"MM_BIN_INQ", MM_BIN_INQ}, {"MM_LIMIT_SET", MM_LIMIT_SET},
{"MM_LIMIT_INQ", MM_LIMIT_INQ}, {"MM_CODE_REG", MM_CODE_REG},
{"MM_CODE_INQ", MM_CODE_INQ}, {"MM_ISSUER_REG", MM_ISSUER_REG},
{"MM_ISSUER_INQ", MM_ISSUER_INQ}, {"MM_MERCH_STATUS", MM_MERCH_STATUS},
{"MM_MERCH_ACCT", MM_MERCH_ACCT}, {"MM_MERCH_GRADE", MM_MERCH_GRADE},
{"MM_TERMINAL_REG", MM_TERMINAL_REG}, {"MM_TERMINAL_INQ", MM_TERMINAL_INQ},
{"MM_MCC_REG", MM_MCC_REG}, {"MM_HOLIDAY_REG", MM_HOLIDAY_REG},
{"MM_FX_REG", MM_FX_REG}, {"MM_FX_INQ", MM_FX_INQ},
{"MM_MERCH_BULK", MM_MERCH_BULK}, {"MM_VALIDATE", MM_VALIDATE},
{"MM_MERCH_SUM", MM_MERCH_SUM}, {"MM_CODE_CACHE", MM_CODE_CACHE},
{"MM_STATUS", MM_STATUS}, {"MM_CONFIRM", MM_CONFIRM},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("mm_svr: tpopen 실패: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("mm_svr: tpadvertise(%s) 실패: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("mm_svr: mm 마스터 서비스 %d개 광고 완료, RM open", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("mm_svr: tpsvrdone");
}
/* ========================================================================= */
/* 1. MM_MERCH_REG - 가맹점 확장 마스터 등록 (dbio upsert + 이력). */
/* ========================================================================= */
void MM_MERCH_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], name[128], grade[8], acct[64];
gets_(b, T_MERCHANT, merch, sizeof(merch));
if (merch[0] == 0) { userlog("MM_MERCH_REG: T_MERCHANT 누락"); FAIL(b); }
gets_(b, T_STR1, name, sizeof(name)); if (name[0] == 0) strcpy(name, "무명가맹점");
gets_(b, T_STR2, grade, sizeof(grade)); if (grade[0] == 0) strcpy(grade, "C");
gets_(b, T_STR3, acct, sizeof(acct));
if (mmdb_insert_merchant(merch, name, grade, acct, "ACTIVE") < 0) FAIL(b);
if (mmdb_insert_merch_hist(merch, "REG", "", grade, "신규 등록") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "ACTIVE", 0L);
userlog("MM_MERCH_REG merch=%s grade=%s 등록", merch, grade);
OK(b);
}
/* 2. MM_MERCH_UPD - 가맹점명/계좌 수정 (직접 UPDATE + 이력). */
void MM_MERCH_UPD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], name[128], acct[64];
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_name[128], h_acct[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STR1, name, sizeof(name)); if (name[0] == 0) strcpy(name, "무명가맹점");
gets_(b, T_STR3, acct, sizeof(acct));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
strncpy(h_acct, acct, sizeof(h_acct)-1); h_acct[sizeof(h_acct)-1] = 0;
EXEC SQL UPDATE mm_merchant_ext
SET name = :h_name, settle_acct = :h_acct, updated_at = now()
WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("MM_MERCH_UPD FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b);
}
if (mmdb_insert_merch_hist(merch, "UPD", "", name, "정보 수정") < 0) FAIL(b);
OK(b);
}
/* 3. MM_MERCH_INQ - 가맹점 마스터 조회 (dbio). */
void MM_MERCH_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], name[64], grade[8], status[16];
gets_(b, T_MERCHANT, merch, sizeof(merch));
if (mmdb_get_merchant(merch, name, grade, status) < 0) FAIL(b);
Bchg(b, T_STR1, 0, name, 0L);
Bchg(b, T_STR2, 0, grade, 0L);
Bchg(b, T_STATUS, 0, status, 0L);
OK(b);
}
/* 4. MM_MERCH_DEL - 가맹점 논리 삭제 (상태 DELETED + 이력). */
void MM_MERCH_DEL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
gets_(b, T_MERCHANT, merch, sizeof(merch));
if (mmdb_update_merchant_status(merch, "DELETED") < 0) FAIL(b);
if (mmdb_insert_merch_hist(merch, "DEL", "ACTIVE", "DELETED", "논리 삭제") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "DELETED", 0L);
OK(b);
}
/* 5. MM_FEERATE_SET - 수수료율 설정 (dbio upsert + 이력). */
void MM_FEERATE_SET(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], ctype[16];
long bps;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STR4, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "CREDIT");
bps = getl(b, T_ARG1); if (bps <= 0) bps = 25;
if (mmdb_set_fee_rate(merch, ctype, bps) < 0) FAIL(b);
userlog("MM_FEERATE_SET merch=%s type=%s bps=%ld", merch, ctype, bps);
OK(b);
}
/* 6. MM_FEERATE_INQ - 수수료율 조회 (dbio). */
void MM_FEERATE_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], ctype[16];
long bps = 25;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STR4, ctype, sizeof(ctype)); if (ctype[0] == 0) strcpy(ctype, "CREDIT");
if (mmdb_get_fee_rate(merch, ctype, &bps) < 0) FAIL(b);
setl(b, T_ARG1, bps);
OK(b);
}
/* 7. MM_FEERATE_HIST - 수수료율 변경 이력 건수 (dbio). */
void MM_FEERATE_HIST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
long cnt;
gets_(b, T_MERCHANT, merch, sizeof(merch));
cnt = mmdb_feerate_hist_count(merch);
if (cnt < 0) FAIL(b);
setl(b, T_COUNT, cnt);
OK(b);
}
/* 8. MM_BIN_REG - BIN 마스터 등록. */
void MM_BIN_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bin[16], issuer[32], brand[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bin[16], h_issuer[32], h_brand[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, bin, sizeof(bin));
if (bin[0] == 0) { userlog("MM_BIN_REG: T_KEY1(bin) 누락"); FAIL(b); }
gets_(b, T_ISSUER, issuer, sizeof(issuer)); if (issuer[0] == 0) strcpy(issuer, "UNKNOWN");
gets_(b, T_STR5, brand, sizeof(brand)); if (brand[0] == 0) strcpy(brand, "LOCAL");
strncpy(h_bin, bin, sizeof(h_bin)-1); h_bin[sizeof(h_bin)-1] = 0;
strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0;
strncpy(h_brand, brand, sizeof(h_brand)-1); h_brand[sizeof(h_brand)-1] = 0;
EXEC SQL INSERT INTO mm_bin (bin, issuer_cd, card_brand)
VALUES (:h_bin, :h_issuer, :h_brand)
ON CONFLICT (bin) DO UPDATE
SET issuer_cd = EXCLUDED.issuer_cd, card_brand = EXCLUDED.card_brand;
if (sqlca.sqlcode < 0) { userlog("MM_BIN_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 9. MM_BIN_INQ - BIN 조회. */
void MM_BIN_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bin[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bin[16], h_issuer[32], h_status[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, bin, sizeof(bin));
strncpy(h_bin, bin, sizeof(h_bin)-1); h_bin[sizeof(h_bin)-1] = 0;
EXEC SQL SELECT issuer_cd, status INTO :h_issuer, :h_status
FROM mm_bin WHERE bin = :h_bin;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_ISSUER, 0, h_issuer, 0L);
Bchg(b, T_STATUS, 0, h_status, 0L);
OK(b);
}
/* 10. MM_LIMIT_SET - 가맹점 한도 설정 (upsert). */
void MM_LIMIT_SET(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_daily, h_monthly, h_pertxn;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
char merch[64];
gets_(b, T_MERCHANT, merch, sizeof(merch));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
h_daily = getl(b, T_AMT1); if (h_daily <= 0) h_daily = 100000000;
h_monthly = getl(b, T_AMT2); if (h_monthly <= 0) h_monthly = h_daily * 30;
h_pertxn = getl(b, T_AMT3); if (h_pertxn <= 0) h_pertxn = 10000000;
EXEC SQL INSERT INTO mm_limit
(merchant_id, daily_limit, monthly_limit, per_txn_limit)
VALUES (:h_merch, :h_daily, :h_monthly, :h_pertxn)
ON CONFLICT (merchant_id) DO UPDATE
SET daily_limit = EXCLUDED.daily_limit, monthly_limit = EXCLUDED.monthly_limit,
per_txn_limit = EXCLUDED.per_txn_limit, updated_at = now();
if (sqlca.sqlcode < 0) { userlog("MM_LIMIT_SET FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 11. MM_LIMIT_INQ - 가맹점 한도 조회. */
void MM_LIMIT_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_daily, h_monthly, h_pertxn;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
char merch[64];
gets_(b, T_MERCHANT, merch, sizeof(merch));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT daily_limit, monthly_limit, per_txn_limit
INTO :h_daily, :h_monthly, :h_pertxn FROM mm_limit WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_AMT1, h_daily); setl(b, T_AMT2, h_monthly); setl(b, T_AMT3, h_pertxn);
OK(b);
}
/* 12. MM_CODE_REG - 공통코드 등록. */
void MM_CODE_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32], code[32], name[64];
EXEC SQL BEGIN DECLARE SECTION;
char h_grp[32], h_code[32], h_name[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "GEN");
gets_(b, T_KEY1, code, sizeof(code));
if (code[0] == 0) { userlog("MM_CODE_REG: T_KEY1(code) 누락"); FAIL(b); }
gets_(b, T_STR2, name, sizeof(name));
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
strncpy(h_code, code, sizeof(h_code)-1); h_code[sizeof(h_code)-1] = 0;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
EXEC SQL INSERT INTO mm_code (code_group, code, code_name)
VALUES (:h_grp, :h_code, :h_name)
ON CONFLICT (code_group, code) DO UPDATE SET code_name = EXCLUDED.code_name;
if (sqlca.sqlcode < 0) { userlog("MM_CODE_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 13. MM_CODE_INQ - 공통코드 조회. */
void MM_CODE_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32], code[32];
EXEC SQL BEGIN DECLARE SECTION;
char h_grp[32], h_code[32], h_name[64], h_status[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "GEN");
gets_(b, T_KEY1, code, sizeof(code));
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
strncpy(h_code, code, sizeof(h_code)-1); h_code[sizeof(h_code)-1] = 0;
EXEC SQL SELECT code_name, status INTO :h_name, :h_status
FROM mm_code WHERE code_group = :h_grp AND code = :h_code;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_STR2, 0, h_name, 0L);
Bchg(b, T_STATUS, 0, h_status, 0L);
OK(b);
}
/* 14. MM_ISSUER_REG - 발급사 마스터 등록. */
void MM_ISSUER_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char cd[32], name[64], country[8];
EXEC SQL BEGIN DECLARE SECTION;
char h_cd[32], h_name[64], h_country[8];
EXEC SQL END DECLARE SECTION;
gets_(b, T_ISSUER, cd, sizeof(cd));
if (cd[0] == 0) { userlog("MM_ISSUER_REG: T_ISSUER 누락"); FAIL(b); }
gets_(b, T_STR1, name, sizeof(name));
gets_(b, T_STR2, country, sizeof(country)); if (country[0] == 0) strcpy(country, "KR");
strncpy(h_cd, cd, sizeof(h_cd)-1); h_cd[sizeof(h_cd)-1] = 0;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
strncpy(h_country, country, sizeof(h_country)-1); h_country[sizeof(h_country)-1] = 0;
EXEC SQL INSERT INTO mm_issuer (issuer_cd, issuer_name, country)
VALUES (:h_cd, :h_name, :h_country)
ON CONFLICT (issuer_cd) DO UPDATE
SET issuer_name = EXCLUDED.issuer_name, country = EXCLUDED.country;
if (sqlca.sqlcode < 0) { userlog("MM_ISSUER_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 15. MM_ISSUER_INQ - 발급사 조회. */
void MM_ISSUER_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char cd[32];
EXEC SQL BEGIN DECLARE SECTION;
char h_cd[32], h_name[64], h_status[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_ISSUER, cd, sizeof(cd));
strncpy(h_cd, cd, sizeof(h_cd)-1); h_cd[sizeof(h_cd)-1] = 0;
EXEC SQL SELECT issuer_name, status INTO :h_name, :h_status
FROM mm_issuer WHERE issuer_cd = :h_cd;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_STR1, 0, h_name, 0L);
Bchg(b, T_STATUS, 0, h_status, 0L);
OK(b);
}
/* 16. MM_MERCH_STATUS - 가맹점 상태 변경 (dbio + 이력). */
void MM_MERCH_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], status[16];
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STATUS, status, sizeof(status)); if (status[0] == 0) strcpy(status, "HOLD");
if (mmdb_update_merchant_status(merch, status) < 0) FAIL(b);
if (mmdb_insert_merch_hist(merch, "STATUS", "", status, "상태 변경") < 0) FAIL(b);
OK(b);
}
/* 17. MM_MERCH_ACCT - 정산계좌 변경 (직접 UPDATE + 이력). */
void MM_MERCH_ACCT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], acct[64];
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_acct[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STR3, acct, sizeof(acct));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_acct, acct, sizeof(h_acct)-1); h_acct[sizeof(h_acct)-1] = 0;
EXEC SQL UPDATE mm_merchant_ext SET settle_acct = :h_acct, updated_at = now()
WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
if (mmdb_insert_merch_hist(merch, "ACCT", "", acct, "계좌 변경") < 0) FAIL(b);
OK(b);
}
/* 18. MM_MERCH_GRADE - 가맹점 등급 변경 (dbio + 이력). */
void MM_MERCH_GRADE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], grade[8];
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STR2, grade, sizeof(grade)); if (grade[0] == 0) strcpy(grade, "C");
if (mmdb_update_merchant_grade(merch, grade) < 0) FAIL(b);
if (mmdb_insert_merch_hist(merch, "GRADE", "", grade, "등급 변경") < 0) FAIL(b);
Bchg(b, T_STR2, 0, grade, 0L);
OK(b);
}
/* 19. MM_TERMINAL_REG - 단말 마스터 등록. */
void MM_TERMINAL_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char term[32], merch[64], model[32];
EXEC SQL BEGIN DECLARE SECTION;
char h_term[32], h_merch[64], h_model[32];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR5, term, sizeof(term));
if (term[0] == 0) { userlog("MM_TERMINAL_REG: T_STR5(terminal) 누락"); FAIL(b); }
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_STR6, model, sizeof(model)); if (model[0] == 0) strcpy(model, "STD");
strncpy(h_term, term, sizeof(h_term)-1); h_term[sizeof(h_term)-1] = 0;
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_model, model, sizeof(h_model)-1); h_model[sizeof(h_model)-1] = 0;
EXEC SQL INSERT INTO mm_terminal (terminal_id, merchant_id, model)
VALUES (:h_term, :h_merch, :h_model)
ON CONFLICT (terminal_id) DO UPDATE
SET merchant_id = EXCLUDED.merchant_id, model = EXCLUDED.model;
if (sqlca.sqlcode < 0) { userlog("MM_TERMINAL_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 20. MM_TERMINAL_INQ - 단말 조회. */
void MM_TERMINAL_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char term[32];
EXEC SQL BEGIN DECLARE SECTION;
char h_term[32], h_merch[64], h_status[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR5, term, sizeof(term));
strncpy(h_term, term, sizeof(h_term)-1); h_term[sizeof(h_term)-1] = 0;
EXEC SQL SELECT merchant_id, status INTO :h_merch, :h_status
FROM mm_terminal WHERE terminal_id = :h_term;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_MERCHANT, 0, h_merch, 0L);
Bchg(b, T_STATUS, 0, h_status, 0L);
OK(b);
}
/* 21. MM_MCC_REG - MCC 업종 마스터 등록. */
void MM_MCC_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char mcc[16], name[64];
EXEC SQL BEGIN DECLARE SECTION;
long h_risk;
char h_mcc[16], h_name[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR6, mcc, sizeof(mcc));
if (mcc[0] == 0) { userlog("MM_MCC_REG: T_STR6(mcc) 누락"); FAIL(b); }
gets_(b, T_STR1, name, sizeof(name));
h_risk = getl(b, T_ARG1); if (h_risk <= 0) h_risk = 1;
strncpy(h_mcc, mcc, sizeof(h_mcc)-1); h_mcc[sizeof(h_mcc)-1] = 0;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
EXEC SQL INSERT INTO mm_mcc (mcc, mcc_name, risk_level)
VALUES (:h_mcc, :h_name, :h_risk)
ON CONFLICT (mcc) DO UPDATE
SET mcc_name = EXCLUDED.mcc_name, risk_level = EXCLUDED.risk_level;
if (sqlca.sqlcode < 0) { userlog("MM_MCC_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 22. MM_HOLIDAY_REG - 공휴일 마스터 등록. */
void MM_HOLIDAY_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char hdate[16], name[64];
EXEC SQL BEGIN DECLARE SECTION;
char h_date[16], h_name[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, hdate, sizeof(hdate));
if (hdate[0] == 0) { userlog("MM_HOLIDAY_REG: T_BIZDATE 누락"); FAIL(b); }
gets_(b, T_STR1, name, sizeof(name)); if (name[0] == 0) strcpy(name, "공휴일");
strncpy(h_date, hdate, sizeof(h_date)-1); h_date[sizeof(h_date)-1] = 0;
strncpy(h_name, name, sizeof(h_name)-1); h_name[sizeof(h_name)-1] = 0;
EXEC SQL INSERT INTO mm_holiday (holiday_date, holiday_name)
VALUES (:h_date, :h_name)
ON CONFLICT (holiday_date) DO UPDATE SET holiday_name = EXCLUDED.holiday_name;
if (sqlca.sqlcode < 0) { userlog("MM_HOLIDAY_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 23. MM_FX_REG - 환율 마스터 upsert. */
void MM_FX_REG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ccy[8];
EXEC SQL BEGIN DECLARE SECTION;
long h_rate;
char h_ccy[8];
EXEC SQL END DECLARE SECTION;
gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD");
h_rate = getl(b, T_ARG2); if (h_rate <= 0) h_rate = 1350;
strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0;
EXEC SQL INSERT INTO mm_fx_rate (ccy, rate_bps, base_date)
VALUES (:h_ccy, :h_rate, current_date)
ON CONFLICT (ccy) DO UPDATE
SET rate_bps = EXCLUDED.rate_bps, base_date = current_date, updated_at = now();
if (sqlca.sqlcode < 0) { userlog("MM_FX_REG FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
OK(b);
}
/* 24. MM_FX_INQ - 환율 조회 + 금액 환산 (rate_bps는 100 기준 bps). */
void MM_FX_INQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ccy[8];
long fx_amt, krw;
EXEC SQL BEGIN DECLARE SECTION;
long h_rate;
char h_ccy[8];
EXEC SQL END DECLARE SECTION;
gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD");
fx_amt = getl(b, T_FX_AMT);
strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0;
EXEC SQL SELECT rate_bps INTO :h_rate FROM mm_fx_rate WHERE ccy = :h_ccy;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
krw = fx_amt * h_rate / 100;
setl(b, T_ARG2, h_rate);
setl(b, T_AMOUNT, krw);
OK(b);
}
/* 25. MM_MERCH_BULK - 가맹점 대량 등록 (T_COUNT개 파생 생성). */
void MM_MERCH_BULK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char prefix[48], grade[8];
long n, i, made = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_name[128], h_grade[8];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, prefix, sizeof(prefix)); if (prefix[0] == 0) strcpy(prefix, "MBULK");
gets_(b, T_STR2, grade, sizeof(grade)); if (grade[0] == 0) strcpy(grade, "C");
n = getl(b, T_COUNT); if (n < 1) n = 1; if (n > 1000) n = 1000;
strncpy(h_grade, grade, sizeof(h_grade)-1); h_grade[sizeof(h_grade)-1] = 0;
for (i = 1; i <= n; i++) {
snprintf(h_merch, sizeof(h_merch), "%s-%04ld", prefix, i);
snprintf(h_name, sizeof(h_name), "%s 지점%ld", prefix, i);
EXEC SQL INSERT INTO mm_merchant_ext (merchant_id, name, grade)
VALUES (:h_merch, :h_name, :h_grade)
ON CONFLICT (merchant_id) DO NOTHING;
if (sqlca.sqlcode < 0) { userlog("MM_MERCH_BULK FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
made++;
}
setl(b, T_COUNT, made);
userlog("MM_MERCH_BULK prefix=%s 생성=%ld", prefix, made);
OK(b);
}
/* 26. MM_VALIDATE - 가맹점 마스터 정합성 검사 (존재+수수료율 존재). */
void MM_VALIDATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_mcnt, h_fcnt;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_mcnt FROM mm_merchant_ext
WHERE merchant_id = :h_merch AND status = 'ACTIVE';
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_fcnt FROM mm_fee_rate WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_mcnt == 0) rc = 1; /* 마스터 없음/비활성 */
else if (h_fcnt == 0) rc = 2; /* 수수료율 미설정 */
setl(b, T_RC, rc);
OK(b);
}
/* 27. MM_MERCH_SUM - 상태별 가맹점 수 집계 (ACTIVE 기준). */
void MM_MERCH_SUM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char status[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_status[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STATUS, status, sizeof(status)); if (status[0] == 0) strcpy(status, "ACTIVE");
strncpy(h_status, status, sizeof(h_status)-1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM mm_merchant_ext WHERE status = :h_status;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 28. MM_CODE_CACHE - 코드그룹의 활성 코드 수 (캐시 워밍 시뮬). */
void MM_CODE_CACHE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_grp[32];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR1, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CARD_TYPE");
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM mm_code
WHERE code_group = :h_grp AND status = 'ACTIVE';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 29. MM_STATUS - 서비스 상태 (전체 가맹점 수, RC=0). */
void MM_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT count(*) INTO :h_cnt FROM mm_merchant_ext;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, 0);
Bchg(b, T_MSG, 0, "mm 마스터 서비스 정상", 0L);
OK(b);
}
/* 30. MM_CONFIRM - 가맹점 승인 확정 (상태 ACTIVE + 이력). */
void MM_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
gets_(b, T_MERCHANT, merch, sizeof(merch));
if (mmdb_update_merchant_status(merch, "ACTIVE") < 0) FAIL(b);
if (mmdb_insert_merch_hist(merch, "STATUS", "HOLD", "ACTIVE", "승인 확정") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "ACTIVE", 0L);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,77 @@
/*
* py_filegen_batch.pgc - py 지급파일 생성 배치 (커서 -> 고정길이 파일 + XA).
*
* ATMI client: opens the ECPG XA RM, starts ONE global transaction, inserts a
* py_file header, then cursors승인(A)된 지급건 for a biz_date writing one
* fixed-length record per payout into PAYOUT_<bizdate>.dat and stamping each
* payout with the file_id (status 'F') via the dbio layer, then tpcommit drives
* XA 2PC. Usage: py_filegen_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "py_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
char path[128], rec[128];
FILE *fp;
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_fname[64], h_acct[32];
long h_file, h_pay, h_net, h_cnt, h_sum;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
snprintf(h_fname, sizeof(h_fname), "PAYOUT_%s.dat", bizdate);
snprintf(path, sizeof(path), "/tmp/%s", h_fname);
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum
FROM py_payout WHERE biz_date = :h_bizdate AND status = 'A';
if (sqlca.sqlcode < 0) { fprintf(stderr, "COUNT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
EXEC SQL SELECT nextval('py_file_seq') INTO :h_file;
if (sqlca.sqlcode < 0) { tpabort(0); return 1; }
EXEC SQL INSERT INTO py_file (file_id, biz_date, file_name, record_count, total_amount, status)
VALUES (:h_file, :h_bizdate, :h_fname, :h_cnt, :h_sum, 'G');
if (sqlca.sqlcode < 0) { fprintf(stderr, "FILE INS FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
fp = fopen(path, "w");
if (fp == NULL) { fprintf(stderr, "fopen FAIL: %s\n", path); tpabort(0); return 1; }
EXEC SQL DECLARE fgc CURSOR FOR
SELECT payout_id, account_no, net FROM py_payout
WHERE biz_date = :h_bizdate AND status = 'A'
ORDER BY payout_id;
EXEC SQL OPEN fgc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); fclose(fp); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH fgc INTO :h_pay, :h_acct, :h_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE fgc; fclose(fp); tpabort(0); return 1; }
/* 고정길이 레코드: payout_id(12) account_no(20) net(15) = 47 bytes + NL */
snprintf(rec, sizeof(rec), "%012ld%-20.20s%015ld\n", h_pay, h_acct, h_net);
fputs(rec, fp);
if (pydb_set_file(h_pay, h_file, "F") < 0) {
EXEC SQL CLOSE fgc; fclose(fp); tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE fgc;
fclose(fp);
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_filegen_batch COMMIT: file=%s file_id=%ld records=%ld total=%ld\n",
path, h_file, rows, h_sum);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,52 @@
/*
* py_result_batch.pgc - py 지급결과 반영 배치 (커서 F->P + 집계 + XA).
*
* Cursors over payouts that were filed (status='F') for a biz_date, flips each
* to 지급완료(status='P') and rolls a per-merchant payout summary via the dbio
* layer - all under ONE global XA transaction. Usage: py_result_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "py_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_merch[64];
long h_pay, h_net;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE prc CURSOR FOR
SELECT payout_id, merchant_id, net FROM py_payout
WHERE biz_date = :h_bizdate AND status = 'F'
ORDER BY payout_id;
EXEC SQL OPEN prc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH prc INTO :h_pay, :h_merch, :h_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE prc; tpabort(0); return 1; }
if (pydb_update_status(h_pay, "P") < 0) { EXEC SQL CLOSE prc; tpabort(0); return 1; }
if (pydb_upsert_summary(h_merch, bizdate, 1, h_net) < 0) { EXEC SQL CLOSE prc; tpabort(0); return 1; }
rows++;
}
EXEC SQL CLOSE prc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_result_batch COMMIT: bizdate=%s paid(F->P)=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,44 @@
/*
* py_account_dbio.pgc - 지급계좌(py_account) DB 접근 (libpydbio.a).
*/
#include <string.h>
#include <userlog.h>
#include "py_dbio.h"
int pydb_account_check(const char *acct, char *status_out, long *balance_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_bal;
char h_acct[32], h_status[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
EXEC SQL SELECT status, balance INTO :h_status, :h_bal
FROM py_account WHERE account_no = :h_acct;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("pydb_account_check FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
strncpy(status_out, h_status, 7); status_out[7] = 0;
*balance_out = h_bal;
return 0;
}
int pydb_account_add(const char *acct, long delta)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_delta = delta;
char h_acct[32];
EXEC SQL END DECLARE SECTION;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
EXEC SQL UPDATE py_account SET balance = balance + :h_delta, updated_at = now()
WHERE account_no = :h_acct;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("pydb_account_add FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

26
app/src/py/dbio/py_dbio.h Normal file
View file

@ -0,0 +1,26 @@
/*
* py_dbio.h - py () DB (libpydbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* py_svr and the py batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef PY_DBIO_H
#define PY_DBIO_H
/* payout */
long pydb_next_payout_id(void);
int pydb_insert_payout(long payout_id, const char *merch, const char *acct, long amount,
long fee, long net, const char *status, const char *channel,
const char *bizdate);
int pydb_update_status(long payout_id, const char *status);
int pydb_get_status(long payout_id, char *out /* >= 2 bytes */);
int pydb_set_file(long payout_id, long file_id, const char *status);
int pydb_upsert_summary(const char *merch, const char *bizdate, long cnt, long amount);
long pydb_insert_adjust(long payout_id, long delta, const char *reason, const char *bizdate);
/* account */
int pydb_account_check(const char *acct, char *status_out /* >= 8 */, long *balance_out);
int pydb_account_add(const char *acct, long delta);
#endif /* PY_DBIO_H */

View file

@ -0,0 +1,144 @@
/*
* py_payout_dbio.pgc - 지급(payout)/조정/집계 DB 접근 (libpydbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "py_dbio.h"
long pydb_next_payout_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('py_payout_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("pydb_next_payout_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int pydb_insert_payout(long payout_id, const char *merch, const char *acct, long amount,
long fee, long net, const char *status, const char *channel,
const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = payout_id, h_amount = amount, h_fee = fee, h_net = net;
char h_merch[64], h_acct[32], h_status[8], h_channel[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_acct, acct, sizeof(h_acct) - 1); h_acct[sizeof(h_acct)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_channel, channel, sizeof(h_channel) - 1); h_channel[sizeof(h_channel)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO py_payout
(payout_id, merchant_id, account_no, amount, fee, net, status, channel, biz_date)
VALUES (:h_id, :h_merch, :h_acct, :h_amount, :h_fee, :h_net, :h_status, :h_channel, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("pydb_insert_payout FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int pydb_update_status(long payout_id, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = payout_id;
char h_status[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE py_payout SET status = :h_status, updated_at = now()
WHERE payout_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("pydb_update_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int pydb_get_status(long payout_id, char *out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = payout_id;
char h_status[8];
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT status INTO :h_status FROM py_payout WHERE payout_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("pydb_get_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
out[0] = h_status[0];
out[1] = 0;
return 0;
}
int pydb_set_file(long payout_id, long file_id, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id = payout_id, h_file = file_id;
char h_status[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL UPDATE py_payout SET file_id = :h_file, status = :h_status, updated_at = now()
WHERE payout_id = :h_id;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("pydb_set_file FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int pydb_upsert_summary(const char *merch, const char *bizdate, long cnt, long amount)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt = cnt, h_amt = amount;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO py_summary (merchant_id, biz_date, payout_count, payout_amount)
VALUES (:h_merch, :h_bizdate, :h_cnt, :h_amt)
ON CONFLICT (merchant_id, biz_date) DO UPDATE
SET payout_count = py_summary.payout_count + EXCLUDED.payout_count,
payout_amount = py_summary.payout_amount + EXCLUDED.payout_amount,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("pydb_upsert_summary FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
long pydb_insert_adjust(long payout_id, long delta, const char *reason, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pay = payout_id, h_delta = delta;
char h_reason[128], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_reason, reason, sizeof(h_reason) - 1); h_reason[sizeof(h_reason)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('py_adjust_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO py_adjust (adj_id, payout_id, delta, reason, biz_date)
VALUES (:h_id, :h_pay, :h_delta, :h_reason, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("pydb_insert_adjust FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}

621
app/src/py/py_svr.pgc Normal file
View file

@ -0,0 +1,621 @@
/*
* py_svr.pgc - py (지급) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online py services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct
* 지급 operation with genuine EXEC SQL (directly, or via the linked libpydbio.a
* dbio layer) and common math from libacqcommon.a.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch); there is NO
* EXEC SQL CONNECT. Owner-writes discipline: services in this server never
* tpcall each other; each owns its tables on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "py_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void PAY_FILEGEN(TPSVCINFO *p); void PAY_RESULT(TPSVCINFO *p);
void PAY_REPROC(TPSVCINFO *p); void PAY_HOLD(TPSVCINFO *p);
void PAY_RELEASE(TPSVCINFO *p); void PAY_CANCEL(TPSVCINFO *p);
void PAY_ACCTCHK(TPSVCINFO *p); void PAY_SPLIT(TPSVCINFO *p);
void PAY_INT(TPSVCINFO *p); void PAY_XFERLINK(TPSVCINFO *p);
void PAY_STATUS(TPSVCINFO *p); void PAY_SCHEDULE(TPSVCINFO *p);
void PAY_MERCHSUM(TPSVCINFO *p); void PAY_NETOFF(TPSVCINFO *p);
void PAY_ADVANCE(TPSVCINFO *p); void PAY_RETRY(TPSVCINFO *p);
void PAY_APPROVE(TPSVCINFO *p); void PAY_REJECT(TPSVCINFO *p);
void PAY_RECON(TPSVCINFO *p); void PAY_STATEMENT(TPSVCINFO *p);
void PAY_FEEDED(TPSVCINFO *p); void PAY_ADJUST(TPSVCINFO *p);
void PAY_BULK(TPSVCINFO *p); void PAY_INQUIRY(TPSVCINFO *p);
void PAY_HISTORY(TPSVCINFO *p); void PAY_REVERSE(TPSVCINFO *p);
void PAY_CONFIRM(TPSVCINFO *p); void PAY_REMIT(TPSVCINFO *p);
void PAY_CLOSE(TPSVCINFO *p); void PAY_SUMMARY(TPSVCINFO *p);
/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"PAY_FILEGEN", PAY_FILEGEN}, {"PAY_RESULT", PAY_RESULT},
{"PAY_REPROC", PAY_REPROC}, {"PAY_HOLD", PAY_HOLD},
{"PAY_RELEASE", PAY_RELEASE}, {"PAY_CANCEL", PAY_CANCEL},
{"PAY_ACCTCHK", PAY_ACCTCHK}, {"PAY_SPLIT", PAY_SPLIT},
{"PAY_INT", PAY_INT}, {"PAY_XFERLINK", PAY_XFERLINK},
{"PAY_STATUS", PAY_STATUS}, {"PAY_SCHEDULE", PAY_SCHEDULE},
{"PAY_MERCHSUM", PAY_MERCHSUM}, {"PAY_NETOFF", PAY_NETOFF},
{"PAY_ADVANCE", PAY_ADVANCE}, {"PAY_RETRY", PAY_RETRY},
{"PAY_APPROVE", PAY_APPROVE}, {"PAY_REJECT", PAY_REJECT},
{"PAY_RECON", PAY_RECON}, {"PAY_STATEMENT", PAY_STATEMENT},
{"PAY_FEEDED", PAY_FEEDED}, {"PAY_ADJUST", PAY_ADJUST},
{"PAY_BULK", PAY_BULK}, {"PAY_INQUIRY", PAY_INQUIRY},
{"PAY_HISTORY", PAY_HISTORY}, {"PAY_REVERSE", PAY_REVERSE},
{"PAY_CONFIRM", PAY_CONFIRM}, {"PAY_REMIT", PAY_REMIT},
{"PAY_CLOSE", PAY_CLOSE}, {"PAY_SUMMARY", PAY_SUMMARY},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("py_svr: tpopen FAIL: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("py_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("py_svr: %d py(지급) 서비스 광고 완료, RM 오픈", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("py_svr: tpsvrdone");
}
/* ========================================================================= */
/* 1. PAY_FILEGEN - 지급 파일 헤더 생성: 당일 승인(A) 지급건 집계 -> py_file. */
/* ========================================================================= */
void PAY_FILEGEN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], fname[64];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_cnt, h_sum;
char h_bizdate[16], h_fname[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum
FROM py_payout WHERE biz_date = :h_bizdate AND status = 'A';
if (sqlca.sqlcode < 0) FAIL(b);
snprintf(fname, sizeof(fname), "PAYOUT_%s.dat", bizdate);
strncpy(h_fname, fname, sizeof(h_fname)-1); h_fname[sizeof(h_fname)-1] = 0;
EXEC SQL SELECT nextval('py_file_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO py_file (file_id, biz_date, file_name, record_count, total_amount, status)
VALUES (:h_id, :h_bizdate, :h_fname, :h_cnt, :h_sum, 'G');
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_ID2, h_id);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_sum);
Bchg(b, T_STR1, 0, fname, 0L);
userlog("PAY_FILEGEN file_id=%ld name=%s recs=%ld amt=%ld", h_id, fname, h_cnt, h_sum);
OK(b);
}
/* 2. PAY_RESULT - 지급 결과 반영: 지급건 상태 P(지급완료). */
void PAY_RESULT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long payout_id = getl(b, T_ID1);
if (pydb_update_status(payout_id, "P") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "P", 0L);
OK(b);
}
/* 3. PAY_REPROC - 재처리: 상태 R(요청)으로 되돌림. */
void PAY_REPROC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long payout_id = getl(b, T_ID1);
if (pydb_update_status(payout_id, "R") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 4. PAY_HOLD - 지급 보류. */
void PAY_HOLD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long payout_id = getl(b, T_ID1);
if (pydb_update_status(payout_id, "H") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "H", 0L);
OK(b);
}
/* 5. PAY_RELEASE - 보류 해제(요청 상태로). */
void PAY_RELEASE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long payout_id = getl(b, T_ID1);
if (pydb_update_status(payout_id, "R") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 6. PAY_CANCEL - 지급 취소. */
void PAY_CANCEL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], reason[128];
long payout_id, amt, adj;
payout_id = getl(b, T_ID1);
amt = getl(b, T_AMOUNT);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "CANCEL");
adj = pydb_insert_adjust(payout_id, -amt, reason, bizdate);
if (adj < 0) FAIL(b);
if (pydb_update_status(payout_id, "C") < 0) FAIL(b);
setl(b, T_ADJ, adj);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 7. PAY_ACCTCHK - 지급계좌 유효성 점검 (dbio). */
void PAY_ACCTCHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], st[8];
long bal = 0;
gets_(b, T_KEY1, acct, sizeof(acct));
if (pydb_account_check(acct, st, &bal) < 0) { setl(b, T_RC, 1); FAIL(b); }
setl(b, T_AMT1, bal);
Bchg(b, T_STR1, 0, st, 0L);
setl(b, T_RC, (strcmp(st, "ACTIVE") == 0) ? 0 : 1);
OK(b);
}
/* 8. PAY_SPLIT - 지급 분할: 금액을 N건으로 나눠 지급건 생성. */
void PAY_SPLIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], acct[32], bizdate[16];
long n, amount, i, part, payout_id;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_KEY1, acct, sizeof(acct));
amount = getl(b, T_AMOUNT);
n = getl(b, T_COUNT); if (n < 1) n = 2;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
part = amount / n;
for (i = 0; i < n; i++) {
payout_id = pydb_next_payout_id();
if (payout_id < 0) FAIL(b);
if (pydb_insert_payout(payout_id, merch, acct, part, 0, part, "R", "SPLIT", bizdate) < 0) FAIL(b);
}
setl(b, T_COUNT, n);
OK(b);
}
/* 9. PAY_INT - 선지급 이자 계산 (일할, bps). */
void PAY_INT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long principal, days, bps, interest;
principal = getl(b, T_AMOUNT);
days = getl(b, T_ARG1); if (days < 1) days = 1;
bps = getl(b, T_ARG2); if (bps <= 0) bps = 15; /* 0.15%/일 */
interest = principal * bps * days / 10000;
setl(b, T_AMT1, interest);
setl(b, T_NET, principal - interest);
OK(b);
}
/* 10. PAY_XFERLINK - 이체 연계 원장 기록(py_ledger). */
void PAY_XFERLINK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pay, h_amt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1); h_amt = getl(b, T_AMOUNT);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date)
VALUES (nextval('py_ledger_seq'), :h_pay, 'XFER', :h_amt, :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
OK(b);
}
/* 11. PAY_STATUS - 지급 상태 조회 (dbio). */
void PAY_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long payout_id = getl(b, T_ID1);
char st[4];
if (pydb_get_status(payout_id, st) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, st, 0L);
OK(b);
}
/* 12. PAY_SCHEDULE - 지급 예약 등록(py_schedule). */
void PAY_SCHEDULE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char rundate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pay;
char h_rundate[16];
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1);
gets_(b, T_BIZDATE, rundate, sizeof(rundate));
strncpy(h_rundate, rundate, sizeof(h_rundate)-1); h_rundate[sizeof(h_rundate)-1] = 0;
EXEC SQL SELECT nextval('py_schedule_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO py_schedule (schedule_id, payout_id, run_date, status)
VALUES (:h_id, :h_pay, :h_rundate, 'S');
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_ID2, h_id);
OK(b);
}
/* 13. PAY_MERCHSUM - 가맹점 당일 지급 집계 upsert (dbio). */
void PAY_MERCHSUM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_amt
FROM py_payout WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (pydb_upsert_summary(merch, bizdate, h_cnt, h_amt) < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
OK(b);
}
/* 14. PAY_NETOFF - 상계(net-off): 지급 금액에서 차감액 반영. */
void PAY_NETOFF(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pay, h_off, h_new;
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1);
h_off = getl(b, T_AMOUNT);
EXEC SQL SELECT net INTO :h_new FROM py_payout WHERE payout_id = :h_pay;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
h_new = h_new - h_off;
EXEC SQL UPDATE py_payout SET net = :h_new, updated_at = now() WHERE payout_id = :h_pay;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_NET, h_new);
OK(b);
}
/* 15. PAY_ADVANCE - 선지급(advance) 지급건 생성. */
void PAY_ADVANCE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], acct[32], bizdate[16];
long amount, fee, net, payout_id;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_KEY1, acct, sizeof(acct));
amount = getl(b, T_AMOUNT);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
fee = acq_fee(amount, 100); /* 선지급 수수료 1% */
net = amount - fee;
payout_id = pydb_next_payout_id();
if (payout_id < 0) FAIL(b);
if (pydb_insert_payout(payout_id, merch, acct, amount, fee, net, "A", "ADVANCE", bizdate) < 0) FAIL(b);
setl(b, T_ID1, payout_id);
setl(b, T_FEE, fee);
setl(b, T_NET, net);
OK(b);
}
/* 16. PAY_RETRY - 실패 지급 재시도(J/H -> R). */
void PAY_RETRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pay;
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1);
EXEC SQL UPDATE py_payout SET status = 'R', retry_count = retry_count + 1, updated_at = now()
WHERE payout_id = :h_pay AND status IN ('J', 'H');
if (sqlca.sqlcode < 0) FAIL(b);
if (sqlca.sqlcode == 100) { setl(b, T_RC, 1); FAIL(b); }
setl(b, T_RC, 0);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 17. PAY_APPROVE - 지급 승인. */
void PAY_APPROVE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long payout_id = getl(b, T_ID1);
if (pydb_update_status(payout_id, "A") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "A", 0L);
OK(b);
}
/* 18. PAY_REJECT - 지급 반려 + 사유 기록. */
void PAY_REJECT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char reason[128];
EXEC SQL BEGIN DECLARE SECTION;
long h_pay;
char h_reason[128];
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1);
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "REJECT");
strncpy(h_reason, reason, sizeof(h_reason)-1); h_reason[sizeof(h_reason)-1] = 0;
EXEC SQL UPDATE py_payout SET status = 'J', reject_reason = :h_reason, updated_at = now()
WHERE payout_id = :h_pay;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
Bchg(b, T_STATUS, 0, "J", 0L);
OK(b);
}
/* 19. PAY_RECON - 지급-파일 대사: 파일에 편성된 건수/합계 확인. */
void PAY_RECON(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_file, h_cnt, h_sum;
EXEC SQL END DECLARE SECTION;
h_file = getl(b, T_ID2);
EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum
FROM py_payout WHERE file_id = :h_file;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_sum);
OK(b);
}
/* 20. PAY_STATEMENT - 가맹점 지급 명세: 기간(당일) 지급 합계 집계. */
void PAY_STATEMENT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt, h_fee, h_net;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0)
INTO :h_cnt, :h_amt, :h_fee, :h_net FROM py_payout
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'P';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
setl(b, T_FEE, h_fee);
setl(b, T_NET, h_net);
OK(b);
}
/* 21. PAY_FEEDED - 수수료 공제(fee deduction): net 재계산. */
void PAY_FEEDED(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long amount, bps, fee, net;
EXEC SQL BEGIN DECLARE SECTION;
long h_pay, h_fee, h_net;
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1);
amount = getl(b, T_AMOUNT);
bps = getl(b, T_ARG1); if (bps <= 0) bps = 30;
fee = acq_fee(amount, bps); net = amount - fee;
h_fee = fee; h_net = net;
EXEC SQL UPDATE py_payout SET fee = :h_fee, net = :h_net, updated_at = now()
WHERE payout_id = :h_pay;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_FEE, fee); setl(b, T_NET, net);
OK(b);
}
/* 22. PAY_ADJUST - 지급 조정 기록 + net 반영 (dbio + 직접 SQL). */
void PAY_ADJUST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], reason[128];
long payout_id, delta, adj;
EXEC SQL BEGIN DECLARE SECTION;
long e_pay, e_delta;
EXEC SQL END DECLARE SECTION;
payout_id = getl(b, T_ID1);
delta = getl(b, T_DELTA);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "ADJ");
adj = pydb_insert_adjust(payout_id, delta, reason, bizdate);
if (adj < 0) FAIL(b);
e_pay = payout_id; e_delta = delta;
EXEC SQL UPDATE py_payout SET net = net + :e_delta, updated_at = now()
WHERE payout_id = :e_pay;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
setl(b, T_ADJ, adj);
OK(b);
}
/* 23. PAY_BULK - 대량 지급 생성: N건 반복 등록. */
void PAY_BULK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], acct[32], bizdate[16];
long n, amount, i, payout_id;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_KEY1, acct, sizeof(acct));
amount = getl(b, T_AMOUNT);
n = getl(b, T_COUNT); if (n < 1) n = 1;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
for (i = 0; i < n; i++) {
payout_id = pydb_next_payout_id();
if (payout_id < 0) FAIL(b);
if (pydb_insert_payout(payout_id, merch, acct, amount, 0, amount, "R", "BULK", bizdate) < 0) FAIL(b);
}
setl(b, T_COUNT, n);
OK(b);
}
/* 24. PAY_INQUIRY - 지급 단건 상세 조회. */
void PAY_INQUIRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], acct[32], st[8];
EXEC SQL BEGIN DECLARE SECTION;
long h_pay, h_amt, h_fee, h_net;
char h_merch[64], h_acct[32], h_status[8];
EXEC SQL END DECLARE SECTION;
h_pay = getl(b, T_ID1);
EXEC SQL SELECT merchant_id, account_no, amount, fee, net, status
INTO :h_merch, :h_acct, :h_amt, :h_fee, :h_net, :h_status
FROM py_payout WHERE payout_id = :h_pay;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
strncpy(merch, h_merch, sizeof(merch)-1); merch[sizeof(merch)-1] = 0;
strncpy(acct, h_acct, sizeof(acct)-1); acct[sizeof(acct)-1] = 0;
strncpy(st, h_status, sizeof(st)-1); st[sizeof(st)-1] = 0;
Bchg(b, T_MERCHANT, 0, merch, 0L);
Bchg(b, T_KEY1, 0, acct, 0L);
Bchg(b, T_STATUS, 0, st, 0L);
setl(b, T_AMOUNT, h_amt);
setl(b, T_FEE, h_fee);
setl(b, T_NET, h_net);
OK(b);
}
/* 25. PAY_HISTORY - 가맹점 지급 이력 건수 조회. */
void PAY_HISTORY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM py_payout WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 26. PAY_REVERSE - 지급 역분개(reverse) + 계좌 잔액 원복. */
void PAY_REVERSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32];
long payout_id = getl(b, T_ID1), amount = getl(b, T_AMOUNT);
gets_(b, T_KEY1, acct, sizeof(acct));
if (pydb_update_status(payout_id, "V") < 0) FAIL(b);
if (acct[0] && pydb_account_add(acct, amount) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "V", 0L);
OK(b);
}
/* 27. PAY_CONFIRM - 지급 확정(confirm): 파일 상태 C(확정). */
void PAY_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_file;
EXEC SQL END DECLARE SECTION;
h_file = getl(b, T_ID2);
EXEC SQL UPDATE py_file SET status = 'C', confirmed_at = now() WHERE file_id = :h_file;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
OK(b);
}
/* 28. PAY_REMIT - 실지급 송금: 상태 P + 계좌 잔액 차감. */
void PAY_REMIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char acct[32], st[8];
long payout_id, net, bal = 0;
payout_id = getl(b, T_ID1);
net = getl(b, T_NET);
gets_(b, T_KEY1, acct, sizeof(acct));
if (pydb_account_check(acct, st, &bal) < 0) FAIL(b);
if (bal < net) { setl(b, T_RC, 1); FAIL(b); }
if (pydb_account_add(acct, -net) < 0) FAIL(b);
if (pydb_update_status(payout_id, "P") < 0) FAIL(b);
setl(b, T_RC, 0);
Bchg(b, T_STATUS, 0, "P", 0L);
OK(b);
}
/* 29. PAY_CLOSE - 일마감: 당일 지급완료(P)건 상태 X(마감)로 일괄 전환. */
void PAY_CLOSE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_cnt;
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL UPDATE py_payout SET status = 'X', updated_at = now()
WHERE biz_date = :h_bizdate AND status = 'P';
if (sqlca.sqlcode < 0) FAIL(b);
h_cnt = sqlca.sqlerrd[2];
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 30. PAY_SUMMARY - 가맹점 일별 지급 집계 조회(py_summary). */
void PAY_SUMMARY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT coalesce(payout_count,0), coalesce(payout_amount,0) INTO :h_cnt, :h_amt
FROM py_summary WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (sqlca.sqlcode == 100) { h_cnt = 0; h_amt = 0; }
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_amt);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,59 @@
/*
* rc_diff_batch.pgc - rc 금액 차이 배치 (커서 + XA).
*
* 당일 매입 중 정산 원장(SETTLE) 합계가 net 과 다른 건을 커서로 순회하며 rc_diff
* 에 AMT 차이 라인을 기록한다 (ONE global XA tx). 사용법: rc_diff_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "rc_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_id, h_pid, h_exp, h_act, h_diff;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE rdc CURSOR FOR
SELECT p.purchase_id, p.net,
coalesce((SELECT sum(l.amount) FROM ledger l
WHERE l.purchase_id = p.purchase_id AND l.entry_type = 'SETTLE'), 0)
FROM purchase p
WHERE p.biz_date = :h_bizdate
ORDER BY p.purchase_id;
EXEC SQL OPEN rdc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH rdc INTO :h_pid, :h_exp, :h_act;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE rdc; tpabort(0); return 1; }
if (h_exp == h_act) continue; /* 일치 건은 건너뜀 */
h_diff = h_exp - h_act;
EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE rdc; tpabort(0); return 1; }
EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date)
VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, 'AMT', :h_bizdate);
if (sqlca.sqlcode < 0) { fprintf(stderr, "INSERT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE rdc; tpabort(0); return 1; }
rows++;
}
EXEC SQL CLOSE rdc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> rc_diff_batch COMMIT: bizdate=%s diff_rows=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,64 @@
/*
* rc_match_batch.pgc - rc 3-way 대사 배치 (커서 + XA).
*
* ATMI client: tpinit/tpopen 로 ECPG XA RM 를 열고, tpbegin 으로 ONE global tx
* 를 시작한 뒤, 승인(approval)과 원장(ledger)이 모두 존재하는 당일 매입을 커서로
* 순회하며 rc_match_log 에 3WAY MATCHED 를 (dbio 로) 기록하고, 나머지는 EXCEPTION
* 으로 남긴다. 마지막에 tpcommit 이 XA 2PC 를 구동. 사용법: rc_match_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "rc_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0, exc = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_pid, h_net, h_appr, h_led;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE rmc CURSOR FOR
SELECT p.purchase_id, p.net,
(SELECT count(*) FROM approval a WHERE a.purchase_id = p.purchase_id),
(SELECT count(*) FROM ledger l WHERE l.purchase_id = p.purchase_id)
FROM purchase p
WHERE p.biz_date = :h_bizdate
ORDER BY p.purchase_id;
EXEC SQL OPEN rmc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH rmc INTO :h_pid, :h_net, :h_appr, :h_led;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE rmc; tpabort(0); return 1; }
if (h_appr > 0 && h_led > 0) {
if (rcdb_insert_match(h_pid, "3WAY", h_net, "MATCHED", bizdate) < 0) {
EXEC SQL CLOSE rmc; tpabort(0); return 1;
}
rows++;
} else {
if (rcdb_insert_match(h_pid, "3WAY", h_net, "EXCEPTION", bizdate) < 0) {
EXEC SQL CLOSE rmc; tpabort(0); return 1;
}
exc++;
}
}
EXEC SQL CLOSE rmc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> rc_match_batch COMMIT: bizdate=%s matched=%ld exception=%ld\n", bizdate, rows, exc);
tpclose(); tpterm();
return 0;
}

26
app/src/rc/dbio/rc_dbio.h Normal file
View file

@ -0,0 +1,26 @@
/*
* rc_dbio.h - rc () DB (librcdbio.a).
* ECPG (EXEC SQL) rc_svr / rc .
* XA / EXEC SQL CONNECT .
* : 0 = ok, -1 = SQL error ( sqlca ).
*/
#ifndef RC_DBIO_H
#define RC_DBIO_H
/* 대사 로그 */
long rcdb_next_match_id(void);
long rcdb_insert_match(long pid, const char *mtype, long amount,
const char *status, const char *bizdate);
int rcdb_update_match_status(long pid, const char *from_status, const char *to_status);
long rcdb_count_match(const char *bizdate, const char *status);
/* 미대사 / 예외 */
long rcdb_count_unmatched(const char *bizdate);
long rcdb_insert_exception(long pid, const char *reason, const char *bizdate);
int rcdb_resolve_exception(long pid);
/* 원장 연계 + 요약 upsert */
long rcdb_insert_ledger(long pid, const char *type, long amount, const char *bizdate);
int rcdb_upsert_summary(const char *bizdate, long matched, long unmatched, long gross);
#endif /* RC_DBIO_H */

View file

@ -0,0 +1,68 @@
/*
* rc_ledger_dbio.pgc - 대사 원장 연계 + 요약 upsert DB 접근 (librcdbio.a).
* ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음.
*/
#include <string.h>
#include <userlog.h>
#include "rc_dbio.h"
long rcdb_count_unmatched(const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase p
WHERE p.biz_date = :h_bizdate
AND NOT EXISTS (SELECT 1 FROM ledger l WHERE l.purchase_id = p.purchase_id);
if (sqlca.sqlcode < 0) {
userlog("rcdb_count_unmatched FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_cnt;
}
long rcdb_insert_ledger(long pid, const char *type, long amount, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_pid = pid, h_amount = amount;
char h_type[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, type, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
VALUES (nextval('ledger_seq'), :h_pid, :h_type, :h_amount, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("rcdb_insert_ledger FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int rcdb_upsert_summary(const char *bizdate, long matched, long unmatched, long gross)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_matched = matched, h_unmatched = unmatched, h_gross = gross;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO rc_summary (biz_date, matched_cnt, unmatched_cnt, gross_amount)
VALUES (:h_bizdate, :h_matched, :h_unmatched, :h_gross)
ON CONFLICT (biz_date) DO UPDATE
SET matched_cnt = EXCLUDED.matched_cnt,
unmatched_cnt = EXCLUDED.unmatched_cnt,
gross_amount = EXCLUDED.gross_amount,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("rcdb_upsert_summary FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

View file

@ -0,0 +1,117 @@
/*
* rc_match_dbio.pgc - 대사 로그/예외 DB 접근 (librcdbio.a).
* ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음.
*/
#include <string.h>
#include <userlog.h>
#include "rc_dbio.h"
long rcdb_next_match_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('rc_match_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("rcdb_next_match_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long rcdb_insert_match(long pid, const char *mtype, long amount,
const char *status, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pid = pid, h_amt = amount;
char h_type[24], h_status[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, mtype, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('rc_match_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO rc_match_log (match_id, purchase_id, match_type, amount, status, biz_date)
VALUES (:h_id, :h_pid, :h_type, :h_amt, :h_status, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("rcdb_insert_match FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int rcdb_update_match_status(long pid, const char *from_status, const char *to_status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_pid = pid;
char h_from[16], h_to[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_from, from_status, sizeof(h_from) - 1); h_from[sizeof(h_from)-1] = 0;
strncpy(h_to, to_status, sizeof(h_to) - 1); h_to[sizeof(h_to)-1] = 0;
EXEC SQL UPDATE rc_match_log SET status = :h_to, updated_at = now()
WHERE purchase_id = :h_pid AND status = :h_from;
if (sqlca.sqlcode < 0) {
userlog("rcdb_update_match_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
long rcdb_count_match(const char *bizdate, const char *status)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16], h_status[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM rc_match_log
WHERE biz_date = :h_bizdate AND status = :h_status;
if (sqlca.sqlcode < 0) return -1;
return h_cnt;
}
long rcdb_insert_exception(long pid, const char *reason, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pid = pid;
char h_reason[128], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_reason, reason, sizeof(h_reason) - 1); h_reason[sizeof(h_reason)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('rc_exc_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO rc_exception (exc_id, purchase_id, reason, biz_date)
VALUES (:h_id, :h_pid, :h_reason, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("rcdb_insert_exception FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int rcdb_resolve_exception(long pid)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_pid = pid;
EXEC SQL END DECLARE SECTION;
EXEC SQL UPDATE rc_exception SET resolved = true
WHERE purchase_id = :h_pid AND resolved = false;
if (sqlca.sqlcode < 0) {
userlog("rcdb_resolve_exception FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

View file

@ -1,9 +1,16 @@
/*
* rc_svr.pgc - rc (대사) MODULE SERVER.
*
* Advertises the reconcile services. RECONCILE keeps the proven behavior: it is
* a DISTINCT XA branch (own DB connection) from ACQUIRE, so it writes only its
* own ledger recon row, then tpcall()s SETTLE (st_svr) inside the same global tx.
* ONE binary that tpadvertise()s ALL ~30 online rc(대사) services — the real
* Tuxedo/Enduro-X "one module server, many services" pattern, modeled on ac_svr.
* Every service is a distinct 대사 operation with genuine EXEC SQL (directly, or
* via the linked librcdbio.a dbio layer).
*
* XA: the connection is opened once by tpopen() (ECPG XA switch); there is NO
* EXEC SQL CONNECT. RECONCILE keeps the proven chain behavior: it is a DISTINCT
* XA branch from ACQUIRE, writes its own recon ledger row, then tpcall()s SETTLE
* (st_svr) inside the SAME global tx. Owner-writes: rc services never tpcall each
* other.
*/
#include <stdio.h>
#include <stdlib.h>
@ -13,22 +20,51 @@
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "rc_dbio.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
void RECONCILE(TPSVCINFO *p);
void RC_REMATCH(TPSVCINFO *p);
void RC_UNMATCH(TPSVCINFO *p);
void RC_HOLD(TPSVCINFO *p);
/* ----- service prototypes ------------------------------------------------ */
void RECONCILE(TPSVCINFO *p); void RC_APPR_ACQ(TPSVCINFO *p);
void RC_ACQ_DEP(TPSVCINFO *p); void RC_RETURN(TPSVCINFO *p);
void RC_AMTDIFF(TPSVCINFO *p); void RC_CNTDIFF(TPSVCINFO *p);
void RC_3WAY(TPSVCINFO *p); void RC_TOLERANCE(TPSVCINFO *p);
void RC_REMATCH(TPSVCINFO *p); void RC_HOLD(TPSVCINFO *p);
void RC_UNMATCH(TPSVCINFO *p); void RC_RELEASE(TPSVCINFO *p);
void RC_EXCEPTION(TPSVCINFO *p); void RC_AUTOFIX(TPSVCINFO *p);
void RC_BYISSUER(TPSVCINFO *p); void RC_BYDATE(TPSVCINFO *p);
void RC_BYMERCH(TPSVCINFO *p); void RC_PARTIAL(TPSVCINFO *p);
void RC_DIFFRPT(TPSVCINFO *p); void RC_STATS(TPSVCINFO *p);
void RC_PROGRESS(TPSVCINFO *p); void RC_SUMMARY(TPSVCINFO *p);
void RC_REPROC(TPSVCINFO *p); void RC_DELAY(TPSVCINFO *p);
void RC_LEDGERLINK(TPSVCINFO *p);void RC_ALERT(TPSVCINFO *p);
void RC_MANUAL(TPSVCINFO *p); void RC_ROLLBACK(TPSVCINFO *p);
void RC_CONFIRM(TPSVCINFO *p); void RC_STATUS(TPSVCINFO *p);
/* ----- advertise table --------------------------------------------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"RECONCILE", RECONCILE}, {"RC_REMATCH", RC_REMATCH},
{"RC_UNMATCH", RC_UNMATCH}, {"RC_HOLD", RC_HOLD},
{"RECONCILE", RECONCILE}, {"RC_APPR_ACQ", RC_APPR_ACQ},
{"RC_ACQ_DEP", RC_ACQ_DEP}, {"RC_RETURN", RC_RETURN},
{"RC_AMTDIFF", RC_AMTDIFF}, {"RC_CNTDIFF", RC_CNTDIFF},
{"RC_3WAY", RC_3WAY}, {"RC_TOLERANCE", RC_TOLERANCE},
{"RC_REMATCH", RC_REMATCH}, {"RC_HOLD", RC_HOLD},
{"RC_UNMATCH", RC_UNMATCH}, {"RC_RELEASE", RC_RELEASE},
{"RC_EXCEPTION", RC_EXCEPTION}, {"RC_AUTOFIX", RC_AUTOFIX},
{"RC_BYISSUER", RC_BYISSUER}, {"RC_BYDATE", RC_BYDATE},
{"RC_BYMERCH", RC_BYMERCH}, {"RC_PARTIAL", RC_PARTIAL},
{"RC_DIFFRPT", RC_DIFFRPT}, {"RC_STATS", RC_STATS},
{"RC_PROGRESS", RC_PROGRESS}, {"RC_SUMMARY", RC_SUMMARY},
{"RC_REPROC", RC_REPROC}, {"RC_DELAY", RC_DELAY},
{"RC_LEDGERLINK", RC_LEDGERLINK},{"RC_ALERT", RC_ALERT},
{"RC_MANUAL", RC_MANUAL}, {"RC_ROLLBACK", RC_ROLLBACK},
{"RC_CONFIRM", RC_CONFIRM}, {"RC_STATUS", RC_STATUS},
{NULL, NULL}
};
@ -47,6 +83,9 @@ int tpsvrinit(int argc, char **argv)
void tpsvrdone(void) { tpclose(); userlog("rc_svr: tpsvrdone"); }
/* ========================================================================= */
/* 1. RECONCILE - 대사 접수 (own branch), then XA chain -> SETTLE. (KEEP) */
/* ========================================================================= */
void RECONCILE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -70,7 +109,7 @@ void RECONCILE(TPSVCINFO *p)
FAIL(b);
}
Bchg(b, T_STATUS, 0, "M", 0L);
userlog("RECONCILE pid=%ld matched -> SETTLE", h_pid);
userlog("RECONCILE pid=%ld 대사완료 -> SETTLE", h_pid);
if (tpcall("SETTLE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) {
userlog("RECONCILE: tpcall(SETTLE) FAIL: %s", tpstrerror(tperrno));
@ -79,7 +118,165 @@ void RECONCILE(TPSVCINFO *p)
OK(b);
}
/* RC_REMATCH - 재대사 원장 기록. */
/* 2. RC_APPR_ACQ - 승인-매입 대사 (승인 존재 여부 확인 후 대사 로그). */
void RC_APPR_ACQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long mid;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_cnt, h_net;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT count(*) INTO :h_cnt FROM approval WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt > 0)
mid = rcdb_insert_match(h_pid, "APPR_ACQ", h_net, "MATCHED", bizdate);
else
mid = rcdb_insert_match(h_pid, "APPR_ACQ", h_net, "EXCEPTION", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_ID1, mid);
setl(b, T_RC, h_cnt > 0 ? 0 : 1);
OK(b);
}
/* 3. RC_ACQ_DEP - 매입-입금(원장) 대사: 원장 합계와 net 비교. */
void RC_ACQ_DEP(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long mid;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_net, h_dep;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_dep FROM ledger
WHERE purchase_id = :h_pid AND entry_type IN ('RECON','SETTLE');
if (sqlca.sqlcode < 0) FAIL(b);
mid = rcdb_insert_match(h_pid, "ACQ_DEP", h_dep,
(h_dep == h_net) ? "MATCHED" : "EXCEPTION", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_GROSS, h_dep);
setl(b, T_RC, (h_dep == h_net) ? 0 : 1);
OK(b);
}
/* 4. RC_RETURN - 반품 대사: 반품 원장 + 음수 대사 로그. */
void RC_RETURN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid = getl(b, T_PURCHASE_ID), amt = getl(b, T_AMOUNT), mid;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (rcdb_insert_ledger(pid, "RETURN", -amt, bizdate) < 0) FAIL(b);
mid = rcdb_insert_match(pid, "RETURN", -amt, "MATCHED", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_ID1, mid);
OK(b);
}
/* 5. RC_AMTDIFF - 금액 차이 대사: 기대(net) vs 실제(원장합) 차이 기록. */
void RC_AMTDIFF(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pid, h_exp, h_act, h_diff;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
h_exp = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT coalesce(sum(amount),0) INTO :h_act FROM ledger
WHERE purchase_id = :h_pid AND entry_type = 'SETTLE';
if (sqlca.sqlcode < 0) FAIL(b);
h_diff = h_exp - h_act;
EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date)
VALUES (:h_id, :h_pid, :h_exp, :h_act, :h_diff, 'AMT', :h_bizdate);
if (sqlca.sqlcode < 0) { userlog("RC_AMTDIFF INSERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
setl(b, T_DELTA, h_diff);
OK(b);
}
/* 6. RC_CNTDIFF - 건수 차이 대사: 매입 건수 vs 대사 로그 건수. */
void RC_CNTDIFF(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pcnt, h_mcnt, h_diff;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_mcnt FROM rc_match_log WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
h_diff = h_pcnt - h_mcnt;
EXEC SQL SELECT nextval('rc_diff_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO rc_diff (diff_id, purchase_id, expected_amt, actual_amt, diff_amt, diff_kind, biz_date)
VALUES (:h_id, 0, :h_pcnt, :h_mcnt, :h_diff, 'CNT', :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_diff);
OK(b);
}
/* 7. RC_3WAY - 3-way 대사: 승인+매입+원장 모두 존재 시 MATCHED. */
void RC_3WAY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long mid;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_appr, h_led, h_net;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT count(*) INTO :h_appr FROM approval WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_led FROM ledger WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
mid = rcdb_insert_match(h_pid, "3WAY", h_net,
(h_appr > 0 && h_led > 0) ? "MATCHED" : "EXCEPTION", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_RC, (h_appr > 0 && h_led > 0) ? 0 : 1);
OK(b);
}
/* 8. RC_TOLERANCE - 허용오차 대사: |차이| <= 허용치면 MATCHED. */
void RC_TOLERANCE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid, tol, diff, adiff, mid;
pid = getl(b, T_PURCHASE_ID);
diff = getl(b, T_DELTA);
tol = getl(b, T_ARG1); if (tol <= 0) tol = 100; /* 기본 허용치 100원 */
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
adiff = diff < 0 ? -diff : diff;
mid = rcdb_insert_match(pid, "TOLERANCE", diff,
(adiff <= tol) ? "MATCHED" : "EXCEPTION", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_RC, (adiff <= tol) ? 0 : 1);
OK(b);
}
/* 9. RC_REMATCH - 재대사 원장 기록. (KEEP) */
void RC_REMATCH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -97,7 +294,25 @@ void RC_REMATCH(TPSVCINFO *p)
OK(b);
}
/* RC_UNMATCH - 미대사 건수 조회. */
/* 10. RC_HOLD - 대사 보류 원장 기록. (KEEP) */
void RC_HOLD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_net;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
VALUES (nextval('ledger_seq'), :h_pid, 'RC_HOLD', :h_net, :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
OK(b);
}
/* 11. RC_UNMATCH - 미대사 건수 조회. (KEEP) */
void RC_UNMATCH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -116,21 +331,298 @@ void RC_UNMATCH(TPSVCINFO *p)
OK(b);
}
/* RC_HOLD - 대사 보류 원장 기록. */
void RC_HOLD(TPSVCINFO *p)
/* 12. RC_RELEASE - 대사 보류 해제 (HOLD -> RELEASED). */
void RC_RELEASE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long pid = getl(b, T_PURCHASE_ID);
if (rcdb_update_match_status(pid, "HOLD", "RELEASED") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 13. RC_EXCEPTION - 대사 예외 등록. */
void RC_EXCEPTION(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], reason[128];
long pid = getl(b, T_PURCHASE_ID), eid;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "AMT_MISMATCH");
eid = rcdb_insert_exception(pid, reason, bizdate);
if (eid < 0) FAIL(b);
setl(b, T_ID1, eid);
OK(b);
}
/* 14. RC_AUTOFIX - 자동 보정: 예외 해소 + 보정 원장. */
void RC_AUTOFIX(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid = getl(b, T_PURCHASE_ID), amt = getl(b, T_AMOUNT);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (rcdb_resolve_exception(pid) < 0) FAIL(b);
if (rcdb_insert_ledger(pid, "AUTOFIX", amt, bizdate) < 0) FAIL(b);
if (rcdb_update_match_status(pid, "EXCEPTION", "MATCHED") < 0) FAIL(b);
OK(b);
}
/* 15. RC_BYISSUER - 발급사별 매입 건수 집계. */
void RC_BYISSUER(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], issuer[32];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16], h_issuer[32];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_ISSUER, issuer, sizeof(issuer)); if (issuer[0] == 0) strcpy(issuer, "BC");
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase
WHERE biz_date = :h_bizdate AND issuer = :h_issuer;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 16. RC_BYDATE - 일자별 대사(MATCHED) 집계. */
void RC_BYDATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_net;
long h_cnt, h_sum;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
VALUES (nextval('ledger_seq'), :h_pid, 'RC_HOLD', :h_net, :h_bizdate);
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_cnt, :h_sum
FROM rc_match_log WHERE biz_date = :h_bizdate AND status = 'MATCHED';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_sum);
OK(b);
}
/* 17. RC_BYMERCH - 가맹점별 net 합계 조회. */
void RC_BYMERCH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], merch[64];
EXEC SQL BEGIN DECLARE SECTION;
long h_sum;
char h_bizdate[16], h_merch[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MERCHANT, merch, sizeof(merch));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_sum FROM purchase
WHERE biz_date = :h_bizdate AND merchant_id = :h_merch;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_GROSS, h_sum);
OK(b);
}
/* 18. RC_PARTIAL - 부분 대사 로그. */
void RC_PARTIAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid = getl(b, T_PURCHASE_ID), part = getl(b, T_AMOUNT), mid;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
mid = rcdb_insert_match(pid, "PARTIAL", part, "MATCHED", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_ID1, mid);
OK(b);
}
/* 19. RC_DIFFRPT - 당일 차이 라인 건수 조회. */
void RC_DIFFRPT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_sum;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(diff_amt),0) INTO :h_cnt, :h_sum
FROM rc_diff WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_DELTA, h_sum);
OK(b);
}
/* 20. RC_STATS - MATCHED/EXCEPTION 통계 (dbio count). */
void RC_STATS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long matched, exc;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
matched = rcdb_count_match(bizdate, "MATCHED");
exc = rcdb_count_match(bizdate, "EXCEPTION");
if (matched < 0 || exc < 0) FAIL(b);
setl(b, T_COUNT, matched);
setl(b, T_RC, exc);
OK(b);
}
/* 21. RC_PROGRESS - 진행률(%) = MATCHED / 전체매입 * 100. */
void RC_PROGRESS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_total, h_matched;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_total FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_matched FROM rc_match_log
WHERE biz_date = :h_bizdate AND status = 'MATCHED';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_RC, h_total > 0 ? (h_matched * 100 / h_total) : 0);
OK(b);
}
/* 22. RC_SUMMARY - 대사 요약 upsert (dbio). */
void RC_SUMMARY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long matched, unmatched;
EXEC SQL BEGIN DECLARE SECTION;
long h_gross;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
matched = rcdb_count_match(bizdate, "MATCHED");
unmatched = rcdb_count_unmatched(bizdate);
if (matched < 0 || unmatched < 0) FAIL(b);
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_gross FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (rcdb_upsert_summary(bizdate, matched, unmatched, h_gross) < 0) FAIL(b);
setl(b, T_COUNT, matched);
setl(b, T_GROSS, h_gross);
OK(b);
}
/* 23. RC_REPROC - 재처리: EXCEPTION 대사 로그를 MATCHED 로 되돌림. */
void RC_REPROC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long pid = getl(b, T_PURCHASE_ID);
if (rcdb_update_match_status(pid, "EXCEPTION", "MATCHED") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "M", 0L);
OK(b);
}
/* 24. RC_DELAY - 지연 대사 로그(익일 반영). */
void RC_DELAY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid = getl(b, T_PURCHASE_ID), net = getl(b, T_NET), mid;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
mid = rcdb_insert_match(pid, "DELAY", net, "HOLD", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_ID1, mid);
OK(b);
}
/* 25. RC_LEDGERLINK - 대사 원장 연계 기록 (dbio). */
void RC_LEDGERLINK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid = getl(b, T_PURCHASE_ID), net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (rcdb_insert_ledger(pid, "RC_LINK", net, bizdate) < 0) FAIL(b);
OK(b);
}
/* 26. RC_ALERT - 대사 경보 등록. */
void RC_ALERT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], msg[128], lvl[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pid;
char h_bizdate[16], h_msg[128], h_lvl[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_MSG, msg, sizeof(msg)); if (msg[0] == 0) strcpy(msg, "대사 지연 경보");
gets_(b, T_STR1, lvl, sizeof(lvl)); if (lvl[0] == 0) strcpy(lvl, "WARN");
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_msg, msg, sizeof(h_msg)-1); h_msg[sizeof(h_msg)-1] = 0;
strncpy(h_lvl, lvl, sizeof(h_lvl)-1); h_lvl[sizeof(h_lvl)-1] = 0;
EXEC SQL SELECT nextval('rc_alert_seq') INTO :h_id;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL INSERT INTO rc_alert (alert_id, purchase_id, lvl, message, biz_date)
VALUES (:h_id, :h_pid, :h_lvl, :h_msg, :h_bizdate);
if (sqlca.sqlcode < 0) { userlog("RC_ALERT INSERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); }
setl(b, T_ID1, h_id);
OK(b);
}
/* 27. RC_MANUAL - 수동 대사 로그. */
void RC_MANUAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long pid = getl(b, T_PURCHASE_ID), net = getl(b, T_NET), mid;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
mid = rcdb_insert_match(pid, "MANUAL", net, "MATCHED", bizdate);
if (mid < 0) FAIL(b);
setl(b, T_ID1, mid);
OK(b);
}
/* 28. RC_ROLLBACK - 대사 롤백 (-> ROLLBACK 상태). */
void RC_ROLLBACK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long pid = getl(b, T_PURCHASE_ID);
if (rcdb_update_match_status(pid, "MATCHED", "ROLLBACK") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "X", 0L);
OK(b);
}
/* 29. RC_CONFIRM - 대사 확정 (MATCHED -> CONFIRMED). */
void RC_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long pid = getl(b, T_PURCHASE_ID);
if (rcdb_update_match_status(pid, "MATCHED", "CONFIRMED") < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 30. RC_STATUS - 최근 대사 상태 조회. */
void RC_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_status[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT status INTO :h_status FROM rc_match_log
WHERE purchase_id = :h_pid ORDER BY match_id DESC LIMIT 1;
if (sqlca.sqlcode < 0) FAIL(b);
if (sqlca.sqlcode == 100) strcpy(h_status, "NONE");
Bchg(b, T_STATUS, 0, h_status, 0L);
OK(b);
}

View file

@ -0,0 +1,59 @@
/*
* st_feecalc_batch.pgc - st 수수료 계산 배치 (커서 + XA).
*
* 당일 정산 각 건에 대해 매입 금액 기준 구간요율(dbio)을 적용해 MDR 수수료를
* 계산하고 st_settle_dtl 에 정산 상세를 (dbio 로) 기록한다 (ONE global XA tx).
* 사용법: st_feecalc_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "st_dbio.h"
#include "acq_common.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_merch[64];
long h_sid, h_pid, h_amount;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE sfc CURSOR FOR
SELECT s.settlement_id, s.purchase_id, s.merchant_id, p.amount
FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id
WHERE s.biz_date = :h_bizdate
ORDER BY s.settlement_id;
EXEC SQL OPEN sfc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
long bps, fee;
EXEC SQL FETCH sfc INTO :h_sid, :h_pid, :h_merch, :h_amount;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE sfc; tpabort(0); return 1; }
bps = stdb_lookup_rate("MDR", h_amount);
if (bps < 0) { EXEC SQL CLOSE sfc; tpabort(0); return 1; }
fee = acq_fee(h_amount, bps);
if (stdb_insert_fee_dtl(h_sid, h_pid, h_merch, "MDR", fee, bizdate) < 0) {
EXEC SQL CLOSE sfc; tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE sfc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> st_feecalc_batch COMMIT: bizdate=%s fee_rows=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,56 @@
/*
* st_settle_batch.pgc - st 가맹점 정산 네팅 배치 (GROUP BY 커서 + XA).
*
* ATMI client: tpinit/tpopen 로 ECPG XA RM 를 열고 tpbegin 으로 ONE global tx 를
* 시작한 뒤, 당일 정산(settlement)을 가맹점별로 GROUP BY 하여 net 합계를 구하고
* st_merch_settle 에 upsert(dbio) 한다. 마지막 tpcommit 이 XA 2PC 구동.
* 사용법: st_settle_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "st_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long rows = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16], h_merch[64];
long h_cnt, h_gross, h_fee, h_net;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE ssc CURSOR FOR
SELECT s.merchant_id, count(*), coalesce(sum(p.amount),0),
coalesce(sum(p.fee),0), coalesce(sum(s.net),0)
FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id
WHERE s.biz_date = :h_bizdate
GROUP BY s.merchant_id ORDER BY s.merchant_id;
EXEC SQL OPEN ssc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH ssc INTO :h_merch, :h_cnt, :h_gross, :h_fee, :h_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE ssc; tpabort(0); return 1; }
if (stdb_upsert_merch_settle(h_merch, bizdate, h_gross, h_fee, h_net, h_cnt) < 0) {
EXEC SQL CLOSE ssc; tpabort(0); return 1;
}
rows++;
}
EXEC SQL CLOSE ssc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> st_settle_batch COMMIT: bizdate=%s merchants=%ld\n", bizdate, rows);
tpclose(); tpterm();
return 0;
}

25
app/src/st/dbio/st_dbio.h Normal file
View file

@ -0,0 +1,25 @@
/*
* st_dbio.h - st (/) DB (libstdbio.a).
* ECPG (EXEC SQL) st_svr / st .
* XA / EXEC SQL CONNECT .
* : 0 = ok, -1 = SQL error ( ID/rate ).
*/
#ifndef ST_DBIO_H
#define ST_DBIO_H
/* 정산 상세 / 지급 / 가맹점 집계 (st_settle_dbio.pgc) */
long stdb_insert_fee_dtl(long sid, long pid, const char *merch,
const char *fee_type, long amount, const char *bizdate);
long stdb_insert_payment(const char *merch, long net, const char *paydate,
const char *status, const char *bizdate);
int stdb_update_payment_status(const char *merch, const char *from_status,
const char *to_status, const char *bizdate);
int stdb_upsert_merch_settle(const char *merch, const char *bizdate, long gross,
long fee, long net, long cnt);
/* 수수료 요율 / 조정 (st_fee_dbio.pgc) */
long stdb_lookup_rate(const char *fee_type, long amount); /* 구간 요율(bps), -1=오류 */
long stdb_insert_adjust(long sid, long pid, long delta, const char *reason,
const char *bizdate);
#endif /* ST_DBIO_H */

View file

@ -0,0 +1,51 @@
/*
* st_fee_dbio.pgc - 수수료 구간요율 조회 + 정산 조정 DB 접근 (libstdbio.a).
* ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음.
*/
#include <string.h>
#include <userlog.h>
#include "st_dbio.h"
/* 금액에 해당하는 최상위 구간 요율(bps)을 반환. 없으면 0, SQL 오류면 -1. */
long stdb_lookup_rate(const char *fee_type, long amount)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_bps, h_amt = amount;
char h_type[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, fee_type, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
EXEC SQL SELECT rate_bps INTO :h_bps FROM st_fee_rate
WHERE fee_type = :h_type AND band_min <= :h_amt
ORDER BY band_min DESC LIMIT 1;
if (sqlca.sqlcode == 100) return 0;
if (sqlca.sqlcode < 0) {
userlog("stdb_lookup_rate FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_bps;
}
long stdb_insert_adjust(long sid, long pid, long delta, const char *reason,
const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_sid = sid, h_pid = pid, h_delta = delta;
char h_reason[128], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_reason, reason, sizeof(h_reason) - 1); h_reason[sizeof(h_reason)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('st_adj_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO st_adjust (adj_id, settlement_id, purchase_id, delta, reason, biz_date)
VALUES (:h_id, :h_sid, :h_pid, :h_delta, :h_reason, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("stdb_insert_adjust FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}

View file

@ -0,0 +1,105 @@
/*
* st_settle_dbio.pgc - 정산 상세/지급/가맹점 집계 DB 접근 (libstdbio.a).
* ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음.
*/
#include <string.h>
#include <userlog.h>
#include "st_dbio.h"
long stdb_insert_fee_dtl(long sid, long pid, const char *merch,
const char *fee_type, long amount, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_sid = sid, h_pid = pid, h_amt = amount;
char h_merch[64], h_type[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_type, fee_type, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('st_dtl_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO st_settle_dtl
(dtl_id, settlement_id, purchase_id, merchant_id, fee_type, amount, biz_date)
VALUES (:h_id, :h_sid, :h_pid, :h_merch, :h_type, :h_amt, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("stdb_insert_fee_dtl FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long stdb_insert_payment(const char *merch, long net, const char *paydate,
const char *status, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_net = net;
char h_merch[64], h_paydate[16], h_status[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_paydate, paydate, sizeof(h_paydate) - 1); h_paydate[sizeof(h_paydate)-1] = 0;
strncpy(h_status, status, sizeof(h_status) - 1); h_status[sizeof(h_status)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT nextval('st_pay_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO st_payment (pay_id, merchant_id, net_amount, pay_date, status, biz_date)
VALUES (:h_id, :h_merch, :h_net, :h_paydate, :h_status, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("stdb_insert_payment FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
int stdb_update_payment_status(const char *merch, const char *from_status,
const char *to_status, const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_from[16], h_to[16], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_from, from_status, sizeof(h_from) - 1); h_from[sizeof(h_from)-1] = 0;
strncpy(h_to, to_status, sizeof(h_to) - 1); h_to[sizeof(h_to)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL UPDATE st_payment SET status = :h_to
WHERE merchant_id = :h_merch AND status = :h_from AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) {
userlog("stdb_update_payment_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}
int stdb_upsert_merch_settle(const char *merch, const char *bizdate, long gross,
long fee, long net, long cnt)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_gross = gross, h_fee = fee, h_net = net, h_cnt = cnt;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_merch, merch, sizeof(h_merch) - 1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO st_merch_settle
(merchant_id, biz_date, txn_count, gross_amount, fee_amount, net_amount)
VALUES (:h_merch, :h_bizdate, :h_cnt, :h_gross, :h_fee, :h_net)
ON CONFLICT (merchant_id, biz_date) DO UPDATE
SET txn_count = st_merch_settle.txn_count + EXCLUDED.txn_count,
gross_amount = st_merch_settle.gross_amount + EXCLUDED.gross_amount,
fee_amount = st_merch_settle.fee_amount + EXCLUDED.fee_amount,
net_amount = st_merch_settle.net_amount + EXCLUDED.net_amount,
updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("stdb_upsert_merch_settle FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

View file

@ -1,9 +1,16 @@
/*
* st_svr.pgc - st (정산/수수료) MODULE SERVER.
*
* Advertises the settlement services. SETTLE keeps the proven behavior: its own
* XA branch writes the settlement + settle ledger rows (the purchase status
* transition is owned by the ACQUIRE branch). Terminal service of the 매입 chain.
* ONE binary that tpadvertise()s ALL ~30 online st(정산/수수료) services — the
* real Enduro-X "one module server, many services" pattern, modeled on ac_svr.
* Every service is a distinct 정산/수수료 operation with genuine EXEC SQL (directly,
* or via the linked libstdbio.a dbio layer) plus fee math from libacqcommon.a.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch); there is NO
* EXEC SQL CONNECT. SETTLE keeps the proven behavior: its own XA branch writes
* the settlement + settle ledger rows (the purchase status transition is owned by
* the ACQUIRE branch). Terminal service of the 매입 chain. Owner-writes: st
* services never tpcall each other.
*/
#include <stdio.h>
#include <stdlib.h>
@ -13,7 +20,10 @@
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "acq_common.h"
#include "st_dbio.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
@ -22,14 +32,40 @@ static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
void SETTLE(TPSVCINFO *p);
void ST_MDR(TPSVCINFO *p);
void ST_NETTING(TPSVCINFO *p);
void ST_VAT(TPSVCINFO *p);
/* ----- service prototypes ------------------------------------------------ */
void SETTLE(TPSVCINFO *p); void ST_MDR(TPSVCINFO *p);
void ST_NETTING(TPSVCINFO *p); void ST_VAT(TPSVCINFO *p);
void ST_VANFEE(TPSVCINFO *p); void ST_RELAYFEE(TPSVCINFO *p);
void ST_WHT(TPSVCINFO *p); void ST_ADJUST(TPSVCINFO *p);
void ST_ADVANCE(TPSVCINFO *p); void ST_DELAYINT(TPSVCINFO *p);
void ST_HOLD(TPSVCINFO *p); void ST_RELEASE(TPSVCINFO *p);
void ST_SPLIT(TPSVCINFO *p); void ST_PAYDATE(TPSVCINFO *p);
void ST_BYMERCH(TPSVCINFO *p); void ST_MONTHLY(TPSVCINFO *p);
void ST_OFFSET(TPSVCINFO *p); void ST_DIFFCHK(TPSVCINFO *p);
void ST_RECALC(TPSVCINFO *p); void ST_RATEAPPLY(TPSVCINFO *p);
void ST_REPORT(TPSVCINFO *p); void ST_XFERLINK(TPSVCINFO *p);
void ST_STATS(TPSVCINFO *p); void ST_CANCEL(TPSVCINFO *p);
void ST_LEDGERLINK(TPSVCINFO *p);void ST_BANDRATE(TPSVCINFO *p);
void ST_CAP(TPSVCINFO *p); void ST_FXCONV(TPSVCINFO *p);
void ST_CONFIRM(TPSVCINFO *p); void ST_STATUS(TPSVCINFO *p);
/* ----- advertise table --------------------------------------------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"SETTLE", SETTLE}, {"ST_MDR", ST_MDR},
{"ST_NETTING", ST_NETTING}, {"ST_VAT", ST_VAT},
{"SETTLE", SETTLE}, {"ST_MDR", ST_MDR},
{"ST_NETTING", ST_NETTING}, {"ST_VAT", ST_VAT},
{"ST_VANFEE", ST_VANFEE}, {"ST_RELAYFEE", ST_RELAYFEE},
{"ST_WHT", ST_WHT}, {"ST_ADJUST", ST_ADJUST},
{"ST_ADVANCE", ST_ADVANCE}, {"ST_DELAYINT", ST_DELAYINT},
{"ST_HOLD", ST_HOLD}, {"ST_RELEASE", ST_RELEASE},
{"ST_SPLIT", ST_SPLIT}, {"ST_PAYDATE", ST_PAYDATE},
{"ST_BYMERCH", ST_BYMERCH}, {"ST_MONTHLY", ST_MONTHLY},
{"ST_OFFSET", ST_OFFSET}, {"ST_DIFFCHK", ST_DIFFCHK},
{"ST_RECALC", ST_RECALC}, {"ST_RATEAPPLY", ST_RATEAPPLY},
{"ST_REPORT", ST_REPORT}, {"ST_XFERLINK", ST_XFERLINK},
{"ST_STATS", ST_STATS}, {"ST_CANCEL", ST_CANCEL},
{"ST_LEDGERLINK", ST_LEDGERLINK},{"ST_BANDRATE", ST_BANDRATE},
{"ST_CAP", ST_CAP}, {"ST_FXCONV", ST_FXCONV},
{"ST_CONFIRM", ST_CONFIRM}, {"ST_STATUS", ST_STATUS},
{NULL, NULL}
};
@ -48,6 +84,9 @@ int tpsvrinit(int argc, char **argv)
void tpsvrdone(void) { tpclose(); userlog("st_svr: tpsvrdone"); }
/* ========================================================================= */
/* 1. SETTLE - 정산 + settle 원장 (own branch). 매입 체인 종단. (KEEP) */
/* ========================================================================= */
void SETTLE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -79,7 +118,7 @@ void SETTLE(TPSVCINFO *p)
OK(b);
}
/* ST_MDR - MDR 수수료 원장 기록. */
/* 2. ST_MDR - MDR 수수료 원장 기록. (KEEP) */
void ST_MDR(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -97,7 +136,7 @@ void ST_MDR(TPSVCINFO *p)
OK(b);
}
/* ST_NETTING - 당일 net 합계 조회. */
/* 3. ST_NETTING - 당일 net 합계 조회. (KEEP) */
void ST_NETTING(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -117,7 +156,7 @@ void ST_NETTING(TPSVCINFO *p)
OK(b);
}
/* ST_VAT - 정산 부가세 원장 기록. */
/* 4. ST_VAT - 정산 부가세 원장 기록. (KEEP) */
void ST_VAT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
@ -136,4 +175,454 @@ void ST_VAT(TPSVCINFO *p)
OK(b);
}
/* 5. ST_VANFEE - VAN 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */
void ST_VANFEE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long amount = getl(b, T_AMOUNT), bps, fee, dtl;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
bps = stdb_lookup_rate("VAN", amount);
if (bps < 0) FAIL(b);
fee = acq_fee(amount, bps);
dtl = stdb_insert_fee_dtl(sid, pid, merch, "VAN", fee, bizdate);
if (dtl < 0) FAIL(b);
setl(b, T_FEE, fee);
OK(b);
}
/* 6. ST_RELAYFEE - 중계 수수료: 구간요율 조회 후 정산 상세 기록 (dbio). */
void ST_RELAYFEE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long amount = getl(b, T_AMOUNT), bps, fee, dtl;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
bps = stdb_lookup_rate("RELAY", amount);
if (bps < 0) FAIL(b);
fee = acq_fee(amount, bps);
dtl = stdb_insert_fee_dtl(sid, pid, merch, "RELAY", fee, bizdate);
if (dtl < 0) FAIL(b);
setl(b, T_FEE, fee);
OK(b);
}
/* 7. ST_WHT - 원천징수(수수료의 22%) 정산 상세 기록 (dbio). */
void ST_WHT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long fee = getl(b, T_FEE), tax, dtl;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
tax = acq_wht_amt(fee, 2200);
dtl = stdb_insert_fee_dtl(sid, pid, merch, "WHT", tax, bizdate);
if (dtl < 0) FAIL(b);
setl(b, T_TAX, tax);
OK(b);
}
/* 8. ST_ADJUST - 정산 조정 (가감) 기록 (dbio). */
void ST_ADJUST(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16], reason[128];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long delta = getl(b, T_DELTA), adj;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_REASON, reason, sizeof(reason)); if (reason[0] == 0) strcpy(reason, "ADJ");
adj = stdb_insert_adjust(sid, pid, delta, reason, bizdate);
if (adj < 0) FAIL(b);
setl(b, T_ADJ, adj);
OK(b);
}
/* 9. ST_ADVANCE - 선지급 예약: 지급 레코드(SCHEDULED) 생성 (dbio). */
void ST_ADVANCE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16], paydate[16];
long net = getl(b, T_NET), pay;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_KEY1, paydate, sizeof(paydate));
if (paydate[0] == 0) { strncpy(paydate, bizdate, sizeof(paydate)-1); paydate[sizeof(paydate)-1] = 0; }
pay = stdb_insert_payment(merch, net, paydate, "SCHEDULED", bizdate);
if (pay < 0) FAIL(b);
setl(b, T_ID1, pay);
OK(b);
}
/* 10. ST_DELAYINT - 지연이자(net * bps/10000 * 일수) 정산 상세 기록 (dbio). */
void ST_DELAYINT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long net = getl(b, T_NET), days = getl(b, T_ARG1), bps = getl(b, T_ARG2), intr, dtl;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (days <= 0) days = 1;
if (bps <= 0) bps = 15; /* 일 0.15% 기본 */
intr = acq_fee(net, bps) * days;
dtl = stdb_insert_fee_dtl(sid, pid, merch, "DELAYINT", intr, bizdate);
if (dtl < 0) FAIL(b);
setl(b, T_AMOUNT, intr);
OK(b);
}
/* 11. ST_HOLD - 가맹점 지급 보류 (SCHEDULED -> HELD) (dbio). */
void ST_HOLD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (stdb_update_payment_status(merch, "SCHEDULED", "HELD", bizdate) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "H", 0L);
OK(b);
}
/* 12. ST_RELEASE - 지급 보류 해제 (HELD -> SCHEDULED) (dbio). */
void ST_RELEASE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (stdb_update_payment_status(merch, "HELD", "SCHEDULED", bizdate) < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "R", 0L);
OK(b);
}
/* 13. ST_SPLIT - 분할 지급: net 을 n 회로 나눠 지급 레코드 생성 (dbio 루프). */
void ST_SPLIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long net = getl(b, T_NET), n = getl(b, T_INSTALL_N), part, i, pay;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (n < 1) n = 2;
part = net / n;
for (i = 0; i < n; i++) {
pay = stdb_insert_payment(merch, part, bizdate, "SCHEDULED", bizdate);
if (pay < 0) FAIL(b);
}
setl(b, T_COUNT, n);
OK(b);
}
/* 14. ST_PAYDATE - 지급일 산정(익영업일) 후 지급일 갱신. */
void ST_PAYDATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16], paydate[16];
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_bizdate[16], h_paydate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (acq_next_bizday(bizdate, paydate) < 0) FAIL(b);
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
strncpy(h_paydate, paydate, sizeof(h_paydate)-1); h_paydate[sizeof(h_paydate)-1] = 0;
EXEC SQL UPDATE st_payment SET pay_date = :h_paydate
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'SCHEDULED';
if (sqlca.sqlcode < 0) FAIL(b);
Bchg(b, T_KEY1, 0, paydate, 0L);
OK(b);
}
/* 15. ST_BYMERCH - 가맹점별 정산 집계 upsert (dbio). */
void ST_BYMERCH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_gross, h_fee, h_net;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(p.amount),0), coalesce(sum(p.fee),0), coalesce(sum(s.net),0)
INTO :h_cnt, :h_gross, :h_fee, :h_net
FROM settlement s JOIN purchase p ON p.purchase_id = s.purchase_id
WHERE s.merchant_id = :h_merch AND s.biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (stdb_upsert_merch_settle(merch, bizdate, h_gross, h_fee, h_net, h_cnt) < 0) FAIL(b);
setl(b, T_COUNT, h_cnt); setl(b, T_GROSS, h_gross);
OK(b);
}
/* 16. ST_MONTHLY - 월별 정산 net 합계 (biz_date LIKE 'YYYY-MM%'). */
void ST_MONTHLY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], ym[16], like[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_sum;
char h_merch[64], h_like[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_KEY1, ym, sizeof(ym)); if (ym[0] == 0) strcpy(ym, "2026-07");
snprintf(like, sizeof(like), "%s%%", ym);
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_like, like, sizeof(h_like)-1); h_like[sizeof(h_like)-1] = 0;
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_sum FROM settlement
WHERE merchant_id = :h_merch AND to_char(biz_date, 'YYYY-MM-DD') LIKE :h_like;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_GROSS, h_sum);
OK(b);
}
/* 17. ST_OFFSET - 상계: 음수 조정으로 채권 상계 기록 (dbio). */
void ST_OFFSET(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long amt = getl(b, T_AMOUNT), adj;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
adj = stdb_insert_adjust(sid, pid, -amt, "OFFSET", bizdate);
if (adj < 0) FAIL(b);
setl(b, T_ADJ, adj);
OK(b);
}
/* 18. ST_DIFFCHK - 정산 net vs 매입 net 차이 점검. */
void ST_DIFFCHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_snet, h_pnet, h_diff;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_snet FROM settlement WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
h_diff = h_pnet - h_snet;
setl(b, T_DELTA, h_diff);
setl(b, T_RC, h_diff == 0 ? 0 : 1);
OK(b);
}
/* 19. ST_RECALC - 매입 수수료 기준 정산 net 재계산 + 갱신. */
void ST_RECALC(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_amount, h_fee, h_net;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT amount, fee INTO :h_amount, :h_fee FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b);
h_net = h_amount - h_fee;
EXEC SQL UPDATE settlement SET net = :h_net WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_NET, h_net);
OK(b);
}
/* 20. ST_RATEAPPLY - 구간요율 적용 수수료 계산 + 정산 상세 기록 (dbio). */
void ST_RATEAPPLY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16], ftype[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long amount = getl(b, T_AMOUNT), bps, fee, dtl;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR");
bps = stdb_lookup_rate(ftype, amount);
if (bps < 0) FAIL(b);
fee = acq_fee(amount, bps);
dtl = stdb_insert_fee_dtl(sid, pid, merch, ftype, fee, bizdate);
if (dtl < 0) FAIL(b);
setl(b, T_FEE, fee);
setl(b, T_RC, bps);
OK(b);
}
/* 21. ST_REPORT - 당일 정산 건수/합계 리포트. */
void ST_REPORT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_sum;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_cnt, :h_sum
FROM settlement WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_sum);
OK(b);
}
/* 22. ST_XFERLINK - 이체 연계 원장 기록. */
void ST_XFERLINK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_net;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
VALUES (nextval('ledger_seq'), :h_pid, 'ST_XFER', :h_net, :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
OK(b);
}
/* 23. ST_STATS - 정산 통계(건수/net 합계) 조회. */
void ST_STATS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_sum, h_max;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(net),0), coalesce(max(net),0)
INTO :h_cnt, :h_sum, :h_max FROM settlement WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_GROSS, h_sum);
setl(b, T_AMT1, h_max);
OK(b);
}
/* 24. ST_CANCEL - 정산 취소: 상태 CANCELLED + 취소 조정 (dbio). */
void ST_CANCEL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long adj;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_net;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_net FROM settlement WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL UPDATE settlement SET status = 'CANCELLED' WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
adj = stdb_insert_adjust(0, h_pid, -h_net, "SETTLE_CANCEL", bizdate);
if (adj < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 25. ST_LEDGERLINK - 정산 연계 원장 기록. */
void ST_LEDGERLINK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_net;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID); h_net = getl(b, T_NET);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date)
VALUES (nextval('ledger_seq'), :h_pid, 'SETTLELINK', :h_net, :h_bizdate);
if (sqlca.sqlcode < 0) FAIL(b);
OK(b);
}
/* 26. ST_BANDRATE - 금액 구간 요율(bps) 조회 (dbio). */
void ST_BANDRATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char ftype[16];
long amount = getl(b, T_AMOUNT), bps;
gets_(b, T_STR1, ftype, sizeof(ftype)); if (ftype[0] == 0) strcpy(ftype, "MDR");
bps = stdb_lookup_rate(ftype, amount);
if (bps < 0) FAIL(b);
setl(b, T_RC, bps);
OK(b);
}
/* 27. ST_CAP - 수수료 상한 적용: 상한 초과분을 감액 조정 (dbio). */
void ST_CAP(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long sid = getl(b, T_SETTLE_ID), pid = getl(b, T_PURCHASE_ID);
long fee = getl(b, T_FEE), cap = getl(b, T_ARG1), over, adj;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (cap <= 0) cap = 100000; /* 기본 상한 */
if (fee > cap) {
over = fee - cap;
adj = stdb_insert_adjust(sid, pid, -over, "FEE_CAP", bizdate);
if (adj < 0) FAIL(b);
setl(b, T_FEE, cap);
setl(b, T_DELTA, -over);
} else {
setl(b, T_DELTA, 0);
}
OK(b);
}
/* 28. ST_FXCONV - 외화 환산: fx * rate/100 = KRW. */
void ST_FXCONV(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long fx = getl(b, T_FX_AMT), rate = getl(b, T_ARG1), krw;
if (rate <= 0) rate = 1350; /* 13.50 KRW/unit (bps of 100) */
krw = fx * rate / 100;
setl(b, T_AMOUNT, krw);
OK(b);
}
/* 29. ST_CONFIRM - 정산 확정 (SETTLED -> CONFIRMED). */
void ST_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL UPDATE settlement SET status = 'CONFIRMED'
WHERE purchase_id = :h_pid AND status = 'SETTLED';
if (sqlca.sqlcode < 0) FAIL(b);
Bchg(b, T_STATUS, 0, "C", 0L);
OK(b);
}
/* 30. ST_STATUS - 정산 상태 조회. */
void ST_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_status[16];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT status INTO :h_status FROM settlement
WHERE purchase_id = :h_pid ORDER BY settlement_id DESC LIMIT 1;
if (sqlca.sqlcode < 0) FAIL(b);
if (sqlca.sqlcode == 100) strcpy(h_status, "NONE");
Bchg(b, T_STATUS, 0, h_status, 0L);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,62 @@
/*
* vl_anomaly_batch.pgc - 이상거래(고액/0원) 탐지 배치 (커서 + XA).
*
* Cursors over a biz_date's 매입 whose amount exceeds the AMT_MAX rule threshold
* or is non-positive, and that is not already flagged; each is written to
* vl_suspect (via dbio) under ONE global XA transaction.
* Usage: vl_anomaly_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "vl_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long flagged = 0;
char sev[8] = "ERROR";
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_pid, h_amt, thr = 50000000;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
/* 규칙 임계 로드 (dbio) - 없으면 기본값 유지 */
if (vldb_get_rule("AMT_MAX", &thr, sev) < 0) { thr = 50000000; strcpy(sev, "ERROR"); }
EXEC SQL DECLARE vac CURSOR FOR
SELECT p.purchase_id, p.amount FROM purchase p
WHERE p.biz_date = :h_bizdate
AND (p.amount > :thr OR p.amount <= 0)
AND NOT EXISTS (SELECT 1 FROM vl_suspect s
WHERE s.purchase_id = p.purchase_id AND s.rule_code = 'AMT_MAX')
ORDER BY p.purchase_id;
EXEC SQL OPEN vac;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH vac INTO :h_pid, :h_amt;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE vac; tpabort(0); return 1; }
if (vldb_insert_suspect(h_pid, "AMT_MAX", sev,
(h_amt <= 0) ? "0원/음수 금액" : "고액 이상거래", bizdate) < 0) {
EXEC SQL CLOSE vac; tpabort(0); return 1;
}
flagged++;
}
EXEC SQL CLOSE vac;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> vl_anomaly_batch COMMIT: bizdate=%s 임계=%ld 적출=%ld\n", bizdate, thr, flagged);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,64 @@
/*
* vl_sales_batch.pgc - 매입-정산 대사검증 배치 (커서 JOIN + XA).
*
* ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global
* transaction, an EXEC SQL cursor JOINs 정산완료(status='S') 매입 against its
* settlement rows for a biz_date; any purchase whose settled net != sum of its
* settlement net (or with no settlement) is written to vl_suspect (via the
* linked dbio lib), then tpcommit drives XA 2PC.
* Usage: vl_sales_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include "vl_dbio.h"
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long scanned = 0, flagged = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_bizdate[16];
long h_pid, h_pnet, h_snet;
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
if (tpinit(NULL) < 0) { fprintf(stderr, "tpinit FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpopen() < 0) { fprintf(stderr, "tpopen FAIL: %s\n", tpstrerror(tperrno)); return 1; }
if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
/* 매입(정산완료) LEFT JOIN 정산합계 - 불일치/누락 추출 */
EXEC SQL DECLARE vsc CURSOR FOR
SELECT p.purchase_id, p.net, coalesce(s.snet, 0)
FROM purchase p
LEFT JOIN (SELECT purchase_id, sum(net) AS snet FROM settlement
GROUP BY purchase_id) s ON s.purchase_id = p.purchase_id
WHERE p.biz_date = :h_bizdate AND p.status = 'S'
ORDER BY p.purchase_id;
EXEC SQL OPEN vsc;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH vsc INTO :h_pid, :h_pnet, :h_snet;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE vsc; tpabort(0); return 1; }
scanned++;
if (h_snet != h_pnet) {
if (vldb_insert_suspect(h_pid, "LEDGER_MIS", "CRIT",
"매입-정산 대사 불일치", bizdate) < 0) {
EXEC SQL CLOSE vsc; tpabort(0); return 1;
}
flagged++;
}
}
EXEC SQL CLOSE vsc;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> vl_sales_batch COMMIT: bizdate=%s 검사=%ld 적출=%ld\n", bizdate, scanned, flagged);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,92 @@
/*
* vl_check_dbio.pgc - 의심거래 적재 + 검증 로그 기록 (libvldbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "vl_dbio.h"
long vldb_next_suspect_id(void)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT nextval('vl_suspect_seq') INTO :h_id;
if (sqlca.sqlcode < 0) {
userlog("vldb_next_suspect_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long vldb_insert_suspect(long purchase_id, const char *rule_code,
const char *severity, const char *detail,
const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_pid = purchase_id;
char h_rule[32], h_sev[8], h_detail[128], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_rule, rule_code, sizeof(h_rule) - 1); h_rule[sizeof(h_rule)-1] = 0;
strncpy(h_sev, severity, sizeof(h_sev) - 1); h_sev[sizeof(h_sev)-1] = 0;
strncpy(h_detail, detail, sizeof(h_detail) - 1); h_detail[sizeof(h_detail)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1);h_bizdate[sizeof(h_bizdate)-1]= 0;
EXEC SQL SELECT nextval('vl_suspect_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO vl_suspect
(suspect_id, purchase_id, rule_code, severity, detail, biz_date)
VALUES (:h_id, :h_pid, :h_rule, :h_sev, :h_detail, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("vldb_insert_suspect FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long vldb_insert_check_log(const char *check_type, long target_id,
long result_code, const char *detail,
const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_id, h_target = target_id, h_rc = result_code;
char h_type[32], h_detail[128], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_type, check_type, sizeof(h_type) - 1); h_type[sizeof(h_type)-1] = 0;
strncpy(h_detail, detail, sizeof(h_detail) - 1);h_detail[sizeof(h_detail)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1);h_bizdate[sizeof(h_bizdate)-1]= 0;
EXEC SQL SELECT nextval('vl_checklog_seq') INTO :h_id;
if (sqlca.sqlcode < 0) return -1;
EXEC SQL INSERT INTO vl_check_log
(log_id, check_type, target_id, result_code, detail, biz_date)
VALUES (:h_id, :h_type, :h_target, :h_rc, :h_detail, :h_bizdate);
if (sqlca.sqlcode < 0) {
userlog("vldb_insert_check_log FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_id;
}
long vldb_suspect_count(const char *bizdate)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM vl_suspect
WHERE biz_date = :h_bizdate AND status = 'OPEN';
if (sqlca.sqlcode < 0) {
userlog("vldb_suspect_count FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return h_cnt;
}

27
app/src/vl/dbio/vl_dbio.h Normal file
View file

@ -0,0 +1,27 @@
/*
* vl_dbio.h - vl (/validation) DB (libvldbio.a).
* ECPG (EXEC SQL) functions compiled into a static library and linked into
* vl_svr and the vl batches. They run on the caller's XA branch/connection
* (no EXEC SQL CONNECT); sqlca is the shared per-thread ECPG SQLCA.
* Return convention: 0/>=0 = ok, -1 = SQL error (caller may also inspect sqlca).
*/
#ifndef VL_DBIO_H
#define VL_DBIO_H
/* 의심거래/검증로그 (vl_check_dbio) */
long vldb_next_suspect_id(void);
long vldb_insert_suspect(long purchase_id, const char *rule_code,
const char *severity, const char *detail,
const char *bizdate);
long vldb_insert_check_log(const char *check_type, long target_id,
long result_code, const char *detail,
const char *bizdate);
long vldb_suspect_count(const char *bizdate);
/* 검증규칙 (vl_rule_dbio) */
int vldb_get_rule(const char *rule_code, long *threshold_out,
char *severity_out /* >= 8 */);
int vldb_upsert_rule(const char *rule_code, const char *rule_name,
long threshold, const char *severity);
#endif /* VL_DBIO_H */

View file

@ -0,0 +1,51 @@
/*
* vl_rule_dbio.pgc - 검증 규칙 조회 + 등록 (libvldbio.a).
* ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT.
*/
#include <string.h>
#include <userlog.h>
#include "vl_dbio.h"
int vldb_get_rule(const char *rule_code, long *threshold_out, char *severity_out)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_thr;
char h_rule[32], h_sev[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_rule, rule_code, sizeof(h_rule) - 1); h_rule[sizeof(h_rule)-1] = 0;
EXEC SQL SELECT threshold, severity INTO :h_thr, :h_sev
FROM vl_rule WHERE rule_code = :h_rule AND status = 'ACTIVE';
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
userlog("vldb_get_rule FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
*threshold_out = h_thr;
strncpy(severity_out, h_sev, 7); severity_out[7] = 0;
return 0;
}
int vldb_upsert_rule(const char *rule_code, const char *rule_name,
long threshold, const char *severity)
{
EXEC SQL BEGIN DECLARE SECTION;
long h_thr = threshold;
char h_rule[32], h_name[64], h_sev[8];
EXEC SQL END DECLARE SECTION;
strncpy(h_rule, rule_code, sizeof(h_rule) - 1); h_rule[sizeof(h_rule)-1] = 0;
strncpy(h_name, rule_name, sizeof(h_name) - 1); h_name[sizeof(h_name)-1] = 0;
strncpy(h_sev, severity, sizeof(h_sev) - 1); h_sev[sizeof(h_sev)-1] = 0;
EXEC SQL INSERT INTO vl_rule (rule_code, rule_name, threshold, severity)
VALUES (:h_rule, :h_name, :h_thr, :h_sev)
ON CONFLICT (rule_code) DO UPDATE
SET rule_name = EXCLUDED.rule_name, threshold = EXCLUDED.threshold,
severity = EXCLUDED.severity, updated_at = now();
if (sqlca.sqlcode < 0) {
userlog("vldb_upsert_rule FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc);
return -1;
}
return 0;
}

674
app/src/vl/vl_svr.pgc Normal file
View file

@ -0,0 +1,674 @@
/*
* vl_svr.pgc - vl (정합성검증/validation) MODULE SERVER.
*
* ONE binary that tpadvertise()s ALL ~30 online vl services (the real Tuxedo
* "one module server, many services" pattern). Every service is a distinct
* 검증 operation: it SELECTs the target rows, applies a validation branch, and
* returns an 에러코드 in T_RC (0=정상, >0=검증 실패). Genuine EXEC SQL directly
* or via the linked libvldbio.a dbio layer, plus common math from libacqcommon.a.
*
* XA: the connection is opened once by tpopen() (ECPG XA switch); there is NO
* EXEC SQL CONNECT. Each tpcall runs on this process's XA branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
#include <ubf.h>
#include <userlog.h>
#include <ndebug.h>
#include "acq.fd.h"
#include "vl_dbio.h"
#include "acq_common.h"
/* ----- small UBF helpers ------------------------------------------------- */
static long getl(UBFH *b, BFLDID f) { long v = 0; Bget(b, f, 0, (char *)&v, 0L); return v; }
static void gets_(UBFH *b, BFLDID f, char *out, int cap)
{ BFLDLEN l = cap; out[0] = 0; Bget(b, f, 0, out, &l); }
static void setl(UBFH *b, BFLDID f, long v) { Bchg(b, f, 0, (char *)&v, 0L); }
#define FAIL(b) do { tpreturn(TPFAIL, 0, (char *)(b), 0L, 0L); return; } while (0)
#define OK(b) do { tpreturn(TPSUCCESS, 0, (char *)(b), 0L, 0L); return; } while (0)
/* ----- service prototypes ------------------------------------------------ */
void VL_SALES(TPSVCINFO *p); void VL_CARD(TPSVCINFO *p);
void VL_LIMIT(TPSVCINFO *p); void VL_AMOUNT(TPSVCINFO *p);
void VL_ANOMALY(TPSVCINFO *p); void VL_REQUIRED(TPSVCINFO *p);
void VL_CODEVAL(TPSVCINFO *p); void VL_APPR_ACQ(TPSVCINFO *p);
void VL_DUP(TPSVCINFO *p); void VL_DATE(TPSVCINFO *p);
void VL_FEE(TPSVCINFO *p); void VL_SETTLE(TPSVCINFO *p);
void VL_LEDGER(TPSVCINFO *p); void VL_FORMAT(TPSVCINFO *p);
void VL_REFINTEG(TPSVCINFO *p); void VL_DUPAUTH(TPSVCINFO *p);
void VL_BIN(TPSVCINFO *p); void VL_EXPIRY(TPSVCINFO *p);
void VL_TOLERANCE(TPSVCINFO *p); void VL_MERCH(TPSVCINFO *p);
void VL_STATUS_CHK(TPSVCINFO *p); void VL_BALANCE(TPSVCINFO *p);
void VL_SUMMARY(TPSVCINFO *p); void VL_REPORT(TPSVCINFO *p);
void VL_BATCH_CHK(TPSVCINFO *p); void VL_THRESHOLD(TPSVCINFO *p);
void VL_RULE(TPSVCINFO *p); void VL_CROSSCHK(TPSVCINFO *p);
void VL_STATUS(TPSVCINFO *p); void VL_CONFIRM(TPSVCINFO *p);
/* ----- advertise table --------------------------------------------------- */
static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"VL_SALES", VL_SALES}, {"VL_CARD", VL_CARD},
{"VL_LIMIT", VL_LIMIT}, {"VL_AMOUNT", VL_AMOUNT},
{"VL_ANOMALY", VL_ANOMALY}, {"VL_REQUIRED", VL_REQUIRED},
{"VL_CODEVAL", VL_CODEVAL}, {"VL_APPR_ACQ", VL_APPR_ACQ},
{"VL_DUP", VL_DUP}, {"VL_DATE", VL_DATE},
{"VL_FEE", VL_FEE}, {"VL_SETTLE", VL_SETTLE},
{"VL_LEDGER", VL_LEDGER}, {"VL_FORMAT", VL_FORMAT},
{"VL_REFINTEG", VL_REFINTEG}, {"VL_DUPAUTH", VL_DUPAUTH},
{"VL_BIN", VL_BIN}, {"VL_EXPIRY", VL_EXPIRY},
{"VL_TOLERANCE", VL_TOLERANCE}, {"VL_MERCH", VL_MERCH},
{"VL_STATUS_CHK", VL_STATUS_CHK},{"VL_BALANCE", VL_BALANCE},
{"VL_SUMMARY", VL_SUMMARY}, {"VL_REPORT", VL_REPORT},
{"VL_BATCH_CHK", VL_BATCH_CHK}, {"VL_THRESHOLD", VL_THRESHOLD},
{"VL_RULE", VL_RULE}, {"VL_CROSSCHK", VL_CROSSCHK},
{"VL_STATUS", VL_STATUS}, {"VL_CONFIRM", VL_CONFIRM},
{NULL, NULL}
};
int tpsvrinit(int argc, char **argv)
{
int i;
if (tpopen() < 0) {
userlog("vl_svr: tpopen 실패: %s", tpstrerror(tperrno));
return -1;
}
for (i = 0; SVCS[i].name != NULL; i++) {
if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) {
userlog("vl_svr: tpadvertise(%s) 실패: %s", SVCS[i].name, tpstrerror(tperrno));
return -1;
}
}
userlog("vl_svr: vl 검증 서비스 %d개 광고 완료, RM open", i);
return 0;
}
void tpsvrdone(void)
{
tpclose();
userlog("vl_svr: tpsvrdone");
}
/* ========================================================================= */
/* 1. VL_SALES - 매입 원거래 존재 검증 (RC 1=미존재). */
/* ========================================================================= */
void VL_SALES(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_cnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt == 0) rc = 1;
vldb_insert_check_log("VL_SALES", h_pid, rc, rc ? "매입 미존재" : "OK", bizdate);
setl(b, T_RC, rc);
OK(b);
}
/* 2. VL_CARD - 카드번호 Luhn 검증 (RC 1=체크섬 불일치). */
void VL_CARD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char pan[32];
long rc;
gets_(b, T_STR1, pan, sizeof(pan));
if (pan[0] == 0) { setl(b, T_RC, 2); OK(b); } /* 2=번호 없음 */
rc = acq_luhn_ok(pan) ? 0 : 1;
setl(b, T_RC, rc);
OK(b);
}
/* 3. VL_LIMIT - 매입금액 vs 가맹점 일한도 검증 (RC 1=초과). */
void VL_LIMIT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_amt, h_lim;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
h_amt = getl(b, T_AMOUNT);
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT daily_limit INTO :h_lim FROM merchant WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); } /* 3=가맹점 없음 */
if (h_amt > h_lim) rc = 1;
setl(b, T_RC, rc);
OK(b);
}
/* 4. VL_AMOUNT - 금액 유효성(0<amt<=규칙 AMT_MAX) 검증 (RC 1=0/음수,2=상한초과). */
void VL_AMOUNT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long amt, rc = 0, thr = 50000000;
char sev[8];
amt = getl(b, T_AMOUNT);
if (vldb_get_rule("AMT_MAX", &thr, sev) < 0) thr = 50000000;
if (amt <= 0) rc = 1;
else if (amt > thr) rc = 2;
setl(b, T_RC, rc);
setl(b, T_ARG1, thr);
OK(b);
}
/* 5. VL_ANOMALY - 이상거래 탐지 후 의심거래 적재 (dbio). */
void VL_ANOMALY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long rc = 0, sid = 0, thr = 50000000;
char sev[8];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_amt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT amount INTO :h_amt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); }
if (vldb_get_rule("AMT_MAX", &thr, sev) < 0) { thr = 50000000; strcpy(sev, "ERROR"); }
if (h_amt > thr) {
rc = 1;
sid = vldb_insert_suspect(h_pid, "AMT_MAX", sev, "고액 이상거래", bizdate);
if (sid < 0) FAIL(b);
}
setl(b, T_RC, rc);
setl(b, T_ID1, sid);
OK(b);
}
/* 6. VL_REQUIRED - 필수필드(가맹점/금액) 존재 검증 (RC 비트합). */
void VL_REQUIRED(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
long amt, rc = 0;
gets_(b, T_MERCHANT, merch, sizeof(merch));
amt = getl(b, T_AMOUNT);
if (merch[0] == 0) rc |= 1;
if (amt == 0) rc |= 2;
setl(b, T_RC, rc);
OK(b);
}
/* 7. VL_CODEVAL - 공통코드 유효성 검증 vs mm_code (RC 1=미존재). */
void VL_CODEVAL(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char grp[32], code[32];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_grp[32], h_code[32];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR3, grp, sizeof(grp)); if (grp[0] == 0) strcpy(grp, "CARD_TYPE");
gets_(b, T_KEY1, code, sizeof(code));
strncpy(h_grp, grp, sizeof(h_grp)-1); h_grp[sizeof(h_grp)-1] = 0;
strncpy(h_code, code, sizeof(h_code)-1); h_code[sizeof(h_code)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM mm_code
WHERE code_group = :h_grp AND code = :h_code AND status = 'ACTIVE';
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt == 0) rc = 1;
setl(b, T_RC, rc);
OK(b);
}
/* 8. VL_APPR_ACQ - 승인-매입 매칭 검증 (RC 1=승인 없음). */
void VL_APPR_ACQ(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_cnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT count(*) INTO :h_cnt FROM approval
WHERE purchase_id = :h_pid AND approved = true;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt == 0) rc = 1;
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, rc);
OK(b);
}
/* 9. VL_DUP - 중복 매입 검증 (동일 가맹점/금액/영업일 2건 이상 → RC 1). */
void VL_DUP(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_amt;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
h_amt = getl(b, T_AMOUNT);
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase
WHERE merchant_id = :h_merch AND amount = :h_amt AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt > 1) rc = 1;
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, rc);
OK(b);
}
/* 10. VL_DATE - 영업일 유효성(미래일자 금지) 검증 (RC 1=미래). */
void VL_DATE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_future;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (bizdate[0] == 0) { setl(b, T_RC, 2); OK(b); } /* 2=일자 없음 */
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT CASE WHEN :h_bizdate::date > current_date THEN 1 ELSE 0 END INTO :h_future;
if (sqlca.sqlcode < 0) { setl(b, T_RC, 3); OK(b); } /* 3=형식 오류 */
if (h_future) rc = 1;
setl(b, T_RC, rc);
OK(b);
}
/* 11. VL_FEE - 수수료 재계산 정합 검증 (저장 fee vs MDR 재계산, RC 1=불일치). */
void VL_FEE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0, recompute;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_amount, h_fee, h_bps;
char h_merch[64];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT amount, fee, merchant_id INTO :h_amount, :h_fee, :h_merch
FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); }
EXEC SQL SELECT mdr_bps INTO :h_bps FROM merchant WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) h_bps = 25;
recompute = acq_fee(h_amount, h_bps);
if (recompute != h_fee) rc = 1;
setl(b, T_FEE, recompute);
setl(b, T_DELTA, h_fee - recompute);
setl(b, T_RC, rc);
OK(b);
}
/* 12. VL_SETTLE - 정산금액 정합 검증 (settlement.net vs purchase.net, RC 1=불일치). */
void VL_SETTLE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_pnet, h_snet, h_scnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); }
EXEC SQL SELECT count(*), coalesce(sum(net),0) INTO :h_scnt, :h_snet
FROM settlement WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_scnt == 0) rc = 2; /* 정산 미존재 */
else if (h_snet != h_pnet) rc = 1; /* 금액 불일치 */
setl(b, T_NET, h_pnet);
setl(b, T_RC, rc);
OK(b);
}
/* 13. VL_LEDGER - 원장 합계 정합 검증 (sum(ledger) vs purchase.net, RC 1=불일치). */
void VL_LEDGER(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_pnet, h_lsum, h_lcnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
EXEC SQL SELECT net INTO :h_pnet FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); }
EXEC SQL SELECT count(*), coalesce(sum(amount),0) INTO :h_lcnt, :h_lsum
FROM ledger WHERE purchase_id = :h_pid AND entry_type = 'SETTLELINK';
if (sqlca.sqlcode < 0) FAIL(b);
if (h_lcnt == 0) rc = 2; /* 원장 미기록 */
else if (h_lsum != h_pnet) {
rc = 1;
vldb_insert_suspect(h_pid, "LEDGER_MIS", "CRIT", "원장 합계 불일치", bizdate);
}
setl(b, T_GROSS, h_lsum);
setl(b, T_RC, rc);
OK(b);
}
/* 14. VL_FORMAT - 가맹점ID 형식(길이 4~16) 검증 (RC 1=형식 위반). */
void VL_FORMAT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
long rc = 0, len;
gets_(b, T_MERCHANT, merch, sizeof(merch));
len = (long)strlen(merch);
if (len < 4 || len > 16) rc = 1;
setl(b, T_COUNT, len);
setl(b, T_RC, rc);
OK(b);
}
/* 15. VL_REFINTEG - 참조무결성(purchase.merchant_id ∈ merchant) 검증. */
void VL_REFINTEG(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_cnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT count(*) INTO :h_cnt FROM purchase p
WHERE p.purchase_id = :h_pid
AND NOT EXISTS (SELECT 1 FROM merchant m WHERE m.merchant_id = p.merchant_id);
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt > 0) rc = 1; /* 참조 깨짐 */
setl(b, T_RC, rc);
OK(b);
}
/* 16. VL_DUPAUTH - 중복 승인 검증 (approval 2건 이상 → RC 1). */
void VL_DUPAUTH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_cnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT count(*) INTO :h_cnt FROM approval WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt > 1) rc = 1;
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, rc);
OK(b);
}
/* 17. VL_BIN - BIN 유효성 검증 vs mm_bin (RC 1=미등록,2=비활성). */
void VL_BIN(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bin[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt, h_active;
char h_bin[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_KEY1, bin, sizeof(bin));
strncpy(h_bin, bin, sizeof(h_bin)-1); h_bin[sizeof(h_bin)-1] = 0;
EXEC SQL SELECT count(*), coalesce(sum(CASE WHEN status='ACTIVE' THEN 1 ELSE 0 END),0)
INTO :h_cnt, :h_active FROM mm_bin WHERE bin = :h_bin;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt == 0) rc = 1;
else if (h_active == 0) rc = 2;
setl(b, T_RC, rc);
OK(b);
}
/* 18. VL_EXPIRY - 카드 유효기간(YYYYMM) vs 영업일 검증 (RC 1=만료). */
void VL_EXPIRY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char expiry[8], bizdate[16];
long rc = 0, exp_ym, biz_ym, y, m, d;
gets_(b, T_STR2, expiry, sizeof(expiry));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
if (strlen(expiry) < 6 || sscanf(bizdate, "%ld-%ld-%ld", &y, &m, &d) != 3) {
setl(b, T_RC, 2); OK(b); /* 2=형식 오류 */
}
exp_ym = atol(expiry); /* YYYYMM */
biz_ym = y * 100 + m;
if (exp_ym < biz_ym) rc = 1;
setl(b, T_RC, rc);
OK(b);
}
/* 19. VL_TOLERANCE - 금액 허용오차(amount == fee+net ± tol) 검증 (RC 1=초과). */
void VL_TOLERANCE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0, tol, diff;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_amount, h_fee, h_net;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
tol = getl(b, T_ARG1); if (tol < 0) tol = 0;
EXEC SQL SELECT amount, fee, net INTO :h_amount, :h_fee, :h_net
FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); }
diff = h_amount - (h_fee + h_net);
if (diff < 0) diff = -diff;
if (diff > tol) rc = 1;
setl(b, T_DELTA, diff);
setl(b, T_RC, rc);
OK(b);
}
/* 20. VL_MERCH - 가맹점 상태 검증 (merchant.status != ACTIVE → RC 1). */
void VL_MERCH(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
char h_merch[64], h_status[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
EXEC SQL SELECT status INTO :h_status FROM merchant WHERE merchant_id = :h_merch;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 2); OK(b); }
if (strcmp(h_status, "ACTIVE") != 0) rc = 1;
Bchg(b, T_STATUS, 0, h_status, 0L);
setl(b, T_RC, rc);
OK(b);
}
/* 21. VL_STATUS_CHK - 매입 상태 유효값 검증 (허용집합 밖 → RC 1). */
void VL_STATUS_CHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_cnt;
char h_status[8];
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT status INTO :h_status FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 3); OK(b); }
EXEC SQL SELECT count(*) INTO :h_cnt FROM (SELECT :h_status AS s) t
WHERE t.s IN ('A','M','S','C','H','V');
if (sqlca.sqlcode < 0) FAIL(b);
if (h_cnt == 0) rc = 1; /* 허용되지 않은 상태값 */
Bchg(b, T_STATUS, 0, h_status, 0L);
setl(b, T_RC, rc);
OK(b);
}
/* 22. VL_BALANCE - 가맹점 순액 대차 검증 (sum(purchase.net) vs sum(settlement.net)). */
void VL_BALANCE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char merch[64], bizdate[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pnet, h_snet;
char h_merch[64], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_MERCHANT, merch, sizeof(merch));
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_pnet FROM purchase
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate AND status = 'S';
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT coalesce(sum(net),0) INTO :h_snet FROM settlement
WHERE merchant_id = :h_merch AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_pnet != h_snet) rc = 1;
setl(b, T_GROSS, h_pnet);
setl(b, T_NET, h_snet);
setl(b, T_DELTA, h_pnet - h_snet);
setl(b, T_RC, rc);
OK(b);
}
/* 23. VL_SUMMARY - 당일 미결(OPEN) 의심거래 요약 (dbio). */
void VL_SUMMARY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long cnt;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
cnt = vldb_suspect_count(bizdate);
if (cnt < 0) FAIL(b);
setl(b, T_COUNT, cnt);
setl(b, T_RC, cnt > 0 ? 1 : 0);
OK(b);
}
/* 24. VL_REPORT - 심각도별 의심거래 건수 리포트. */
void VL_REPORT(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char sev[8], bizdate[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
char h_sev[8], h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_STR4, sev, sizeof(sev)); if (sev[0] == 0) strcpy(sev, "CRIT");
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_sev, sev, sizeof(h_sev)-1); h_sev[sizeof(h_sev)-1] = 0;
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_cnt FROM vl_suspect
WHERE severity = :h_sev AND biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
OK(b);
}
/* 25. VL_BATCH_CHK - 배치 정합(purchase 건수 vs merchant_summary.txn_count). */
void VL_BATCH_CHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char bizdate[16];
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pcnt, h_scnt;
char h_bizdate[16];
EXEC SQL END DECLARE SECTION;
gets_(b, T_BIZDATE, bizdate, sizeof(bizdate));
strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0;
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT coalesce(sum(txn_count),0) INTO :h_scnt FROM merchant_summary
WHERE biz_date = :h_bizdate;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_pcnt != h_scnt) rc = 1; /* 집계 누락/불일치 */
setl(b, T_COUNT, h_pcnt);
setl(b, T_GROSS, h_scnt);
setl(b, T_RC, rc);
OK(b);
}
/* 26. VL_THRESHOLD - 규칙 임계치 검사 (dbio get_rule; amount>threshold → RC 1). */
void VL_THRESHOLD(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char rule[32], sev[8];
long amt, thr = 0, rc = 0;
gets_(b, T_STR3, rule, sizeof(rule)); if (rule[0] == 0) strcpy(rule, "AMT_MAX");
amt = getl(b, T_AMOUNT);
if (vldb_get_rule(rule, &thr, sev) < 0) { setl(b, T_RC, 3); OK(b); } /* 3=규칙 없음 */
if (thr > 0 && amt > thr) rc = 1;
setl(b, T_ARG1, thr);
Bchg(b, T_STR4, 0, sev, 0L);
setl(b, T_RC, rc);
OK(b);
}
/* 27. VL_RULE - 검증 규칙 등록/갱신 (dbio upsert). */
void VL_RULE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
char rule[32], name[64], sev[8];
long thr;
gets_(b, T_STR3, rule, sizeof(rule));
if (rule[0] == 0) { userlog("VL_RULE: T_STR3(rule_code) 누락"); FAIL(b); }
gets_(b, T_STR1, name, sizeof(name)); if (name[0] == 0) strcpy(name, "사용자 규칙");
gets_(b, T_STR4, sev, sizeof(sev)); if (sev[0] == 0) strcpy(sev, "WARN");
thr = getl(b, T_ARG1);
if (vldb_upsert_rule(rule, name, thr, sev) < 0) FAIL(b);
OK(b);
}
/* 28. VL_CROSSCHK - 교차검증(매입+가맹점+승인 동시 존재, RC 비트합). */
void VL_CROSSCHK(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
long rc = 0;
EXEC SQL BEGIN DECLARE SECTION;
long h_pid, h_pcnt, h_mcnt, h_acnt;
EXEC SQL END DECLARE SECTION;
h_pid = getl(b, T_PURCHASE_ID);
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_mcnt FROM purchase p
JOIN merchant m ON m.merchant_id = p.merchant_id WHERE p.purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
EXEC SQL SELECT count(*) INTO :h_acnt FROM approval WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) FAIL(b);
if (h_pcnt == 0) rc |= 1; /* 매입 없음 */
if (h_mcnt == 0) rc |= 2; /* 가맹점 참조 깨짐 */
if (h_acnt == 0) rc |= 4; /* 승인 없음 */
setl(b, T_RC, rc);
OK(b);
}
/* 29. VL_STATUS - 서비스 상태 (전체 OPEN 의심거래 수, RC=0). */
void VL_STATUS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_cnt;
EXEC SQL END DECLARE SECTION;
EXEC SQL SELECT count(*) INTO :h_cnt FROM vl_suspect WHERE status = 'OPEN';
if (sqlca.sqlcode < 0) FAIL(b);
setl(b, T_COUNT, h_cnt);
setl(b, T_RC, 0);
Bchg(b, T_MSG, 0, "vl 검증 서비스 정상", 0L);
OK(b);
}
/* 30. VL_CONFIRM - 의심거래 종결 확정 (status CLOSED by suspect_id). */
void VL_CONFIRM(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
EXEC SQL BEGIN DECLARE SECTION;
long h_sid;
EXEC SQL END DECLARE SECTION;
h_sid = getl(b, T_ID1);
EXEC SQL UPDATE vl_suspect SET status = 'CLOSED'
WHERE suspect_id = :h_sid AND status = 'OPEN';
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { setl(b, T_RC, 1); OK(b); }
setl(b, T_RC, 0);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

111
db/schema.d/20-au.sql Normal file
View file

@ -0,0 +1,111 @@
-- au 모듈 (승인/한도) 스키마 - 승인 원장 / 카드 한도 / 부정거래 / 예비승인 / 집계
-- Loaded into PostgreSQL on first boot (docker-entrypoint-initdb.d). Prefix 20.
-- ---------------------------------------------------------------------------
-- 승인 원장 (authorization)
-- status: A=승인, D=거절, C=취소, R=역전, H=보류, M=매입확정(capture),
-- S=정산반영, V=무효(void)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS au_auth_seq;
CREATE TABLE IF NOT EXISTS au_authorization (
auth_id BIGINT PRIMARY KEY,
card_no TEXT NOT NULL,
merchant_id TEXT NOT NULL DEFAULT '',
amount BIGINT NOT NULL,
approval_code TEXT NOT NULL DEFAULT '',
auth_type TEXT NOT NULL DEFAULT 'AUTH', -- AUTH/STANDIN/INSTALL/PREAUTH
status TEXT NOT NULL,
decline_reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
captured_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 카드 한도 (card_limit)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS card_limit (
card_no TEXT PRIMARY KEY,
daily_limit BIGINT NOT NULL DEFAULT 1000000,
used_amount BIGINT NOT NULL DEFAULT 0,
temp_limit BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'ACTIVE',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 부정거래 로그 (fraud_log)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS au_fraud_seq;
CREATE TABLE IF NOT EXISTS au_fraud_log (
fraud_id BIGINT PRIMARY KEY,
auth_id BIGINT NOT NULL DEFAULT 0,
card_no TEXT NOT NULL,
score BIGINT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 블랙리스트 (au_blacklist)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS au_blacklist (
card_no TEXT PRIMARY KEY,
reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 예비승인 (au_preauth) - status: P=예비, C=확정, X=취소
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS au_preauth_seq;
CREATE TABLE IF NOT EXISTS au_preauth (
preauth_id BIGINT PRIMARY KEY,
card_no TEXT NOT NULL,
merchant_id TEXT NOT NULL DEFAULT '',
amount BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT 'P',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 승인 취소 (au_cancel) - FULL / PARTIAL / VOID
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS au_cancel_seq;
CREATE TABLE IF NOT EXISTS au_cancel (
cancel_id BIGINT PRIMARY KEY,
auth_id BIGINT NOT NULL,
cancel_type TEXT NOT NULL,
amount BIGINT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 가맹점 일별 승인 집계 (au_summary) - 배치/서비스가 채움
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS au_summary (
merchant_id TEXT NOT NULL,
biz_date DATE NOT NULL,
auth_count BIGINT NOT NULL DEFAULT 0,
approve_count BIGINT NOT NULL DEFAULT 0,
decline_count BIGINT NOT NULL DEFAULT 0,
auth_amount BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (merchant_id, biz_date)
);
-- Seed card limits + a blacklist entry so AUTH/LIMIT services have counterparties.
INSERT INTO card_limit (card_no, daily_limit, used_amount) VALUES
('4000123412341234', 1000000, 0),
('5100123412341234', 2000000, 0),
('3400123412341234', 5000000, 0)
ON CONFLICT (card_no) DO NOTHING;
INSERT INTO au_blacklist (card_no, reason) VALUES
('4000999999999999', 'REPORTED_STOLEN')
ON CONFLICT (card_no) DO NOTHING;

110
db/schema.d/25-py.sql Normal file
View file

@ -0,0 +1,110 @@
-- py 모듈 (지급) 스키마 - 지급 원장 / 지급계좌 / 지급파일 / 예약 / 조정 / 집계
-- Loaded into PostgreSQL on first boot (docker-entrypoint-initdb.d). Prefix 25.
-- ---------------------------------------------------------------------------
-- 지급 원장 (payout)
-- status: R=요청, H=보류, A=승인, J=반려, F=파일편성, P=지급완료,
-- C=취소, V=역분개, X=마감
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS py_payout_seq;
CREATE TABLE IF NOT EXISTS py_payout (
payout_id BIGINT PRIMARY KEY,
merchant_id TEXT NOT NULL DEFAULT '',
account_no TEXT NOT NULL DEFAULT '',
amount BIGINT NOT NULL,
fee BIGINT NOT NULL DEFAULT 0,
net BIGINT NOT NULL,
status TEXT NOT NULL,
channel TEXT NOT NULL DEFAULT 'STD', -- STD/SPLIT/BULK/ADVANCE
file_id BIGINT NOT NULL DEFAULT 0,
retry_count INTEGER NOT NULL DEFAULT 0,
reject_reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 지급계좌 (py_account)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS py_account (
account_no TEXT PRIMARY KEY,
merchant_id TEXT NOT NULL DEFAULT '',
bank_code TEXT NOT NULL DEFAULT '',
holder TEXT NOT NULL DEFAULT '',
balance BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'ACTIVE',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 지급 파일 (py_file) - status: G=생성, C=확정
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS py_file_seq;
CREATE TABLE IF NOT EXISTS py_file (
file_id BIGINT PRIMARY KEY,
biz_date DATE NOT NULL,
file_name TEXT NOT NULL,
record_count BIGINT NOT NULL DEFAULT 0,
total_amount BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'G',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
confirmed_at TIMESTAMPTZ
);
-- ---------------------------------------------------------------------------
-- 지급 예약 (py_schedule) - status: S=예약, D=실행
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS py_schedule_seq;
CREATE TABLE IF NOT EXISTS py_schedule (
schedule_id BIGINT PRIMARY KEY,
payout_id BIGINT NOT NULL,
run_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'S',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 지급 조정 (py_adjust)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS py_adjust_seq;
CREATE TABLE IF NOT EXISTS py_adjust (
adj_id BIGINT PRIMARY KEY,
payout_id BIGINT NOT NULL,
delta BIGINT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 지급 원장 상세 (py_ledger) - 이체/연계 원장
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS py_ledger_seq;
CREATE TABLE IF NOT EXISTS py_ledger (
ledger_id BIGINT PRIMARY KEY,
payout_id BIGINT NOT NULL,
entry_type TEXT NOT NULL,
amount BIGINT NOT NULL,
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 가맹점 일별 지급 집계 (py_summary) - 배치/서비스가 채움
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS py_summary (
merchant_id TEXT NOT NULL,
biz_date DATE NOT NULL,
payout_count BIGINT NOT NULL DEFAULT 0,
payout_amount BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (merchant_id, biz_date)
);
-- Seed payout accounts so PAY_REMIT/PAY_ACCTCHK have counterparties.
INSERT INTO py_account (account_no, merchant_id, bank_code, holder, balance, status) VALUES
('110-1234-567890', 'M0001', '020', '클라로 커피', 500000000, 'ACTIVE'),
('333-9876-543210', 'M0002', '088', '포지 마트', 500000000, 'ACTIVE'),
('456-0000-111222', 'M0003', '004', '클라로 면세점', 900000000, 'ACTIVE')
ON CONFLICT (account_no) DO NOTHING;

69
db/schema.d/30-rc.sql Normal file
View file

@ -0,0 +1,69 @@
-- rc 모듈 (대사) 상세 테이블. Prefix 30 (00-base 다음, 35-st 이전에 로드).
-- ECPG 서버/배치가 참조하는 대사 로그/예외/차이/경보/요약 테이블 + 시퀀스.
-- ---------------------------------------------------------------------------
-- 대사 로그 (rc_match_log) - 서비스별 대사 결과 1행
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS rc_match_seq;
CREATE TABLE IF NOT EXISTS rc_match_log (
match_id BIGINT PRIMARY KEY,
purchase_id BIGINT NOT NULL,
match_type TEXT NOT NULL, -- APPR_ACQ/ACQ_DEP/3WAY/PARTIAL/MANUAL/RETURN/DELAY/TOLERANCE
amount BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'MATCHED',-- MATCHED/HOLD/EXCEPTION/RELEASED/CONFIRMED/ROLLBACK
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 대사 예외 (rc_exception)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS rc_exc_seq;
CREATE TABLE IF NOT EXISTS rc_exception (
exc_id BIGINT PRIMARY KEY,
purchase_id BIGINT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
resolved BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 대사 차이 (rc_diff) - 금액/건수 차이 리포트 라인
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS rc_diff_seq;
CREATE TABLE IF NOT EXISTS rc_diff (
diff_id BIGINT PRIMARY KEY,
purchase_id BIGINT NOT NULL DEFAULT 0,
expected_amt BIGINT NOT NULL DEFAULT 0,
actual_amt BIGINT NOT NULL DEFAULT 0,
diff_amt BIGINT NOT NULL DEFAULT 0,
diff_kind TEXT NOT NULL DEFAULT 'AMT', -- AMT / CNT
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 대사 경보 (rc_alert)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS rc_alert_seq;
CREATE TABLE IF NOT EXISTS rc_alert (
alert_id BIGINT PRIMARY KEY,
purchase_id BIGINT NOT NULL DEFAULT 0,
lvl TEXT NOT NULL DEFAULT 'INFO',
message TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 대사 요약 (rc_summary) - 영업일 1행 (배치/RC_SUMMARY가 upsert)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS rc_summary (
biz_date DATE PRIMARY KEY,
matched_cnt BIGINT NOT NULL DEFAULT 0,
unmatched_cnt BIGINT NOT NULL DEFAULT 0,
gross_amount BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

77
db/schema.d/35-st.sql Normal file
View file

@ -0,0 +1,77 @@
-- st 모듈 (정산/수수료) 상세 테이블. Prefix 35 (30-rc 다음).
-- ECPG 서버/배치가 참조하는 정산 상세/수수료율/조정/지급/가맹점집계 테이블 + 시퀀스.
-- ---------------------------------------------------------------------------
-- 정산 상세 (st_settle_dtl) - 수수료 종류별 정산 라인
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS st_dtl_seq;
CREATE TABLE IF NOT EXISTS st_settle_dtl (
dtl_id BIGINT PRIMARY KEY,
settlement_id BIGINT NOT NULL DEFAULT 0,
purchase_id BIGINT NOT NULL DEFAULT 0,
merchant_id TEXT NOT NULL DEFAULT '',
fee_type TEXT NOT NULL, -- MDR/VAN/RELAY/WHT/VAT/ADJ/DELAYINT/CAP/RATE
amount BIGINT NOT NULL DEFAULT 0,
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 수수료 요율표 (st_fee_rate) - 구간별 요율 (basis points)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS st_fee_rate (
fee_type TEXT NOT NULL,
band_min BIGINT NOT NULL DEFAULT 0, -- 이 금액 이상에 적용
rate_bps BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (fee_type, band_min)
);
-- ---------------------------------------------------------------------------
-- 정산 조정 (st_adjust) - 가감/상계/취소 조정
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS st_adj_seq;
CREATE TABLE IF NOT EXISTS st_adjust (
adj_id BIGINT PRIMARY KEY,
settlement_id BIGINT NOT NULL DEFAULT 0,
purchase_id BIGINT NOT NULL DEFAULT 0,
delta BIGINT NOT NULL DEFAULT 0,
reason TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 지급 (st_payment) - 가맹점 지급 예정/보류/완료
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS st_pay_seq;
CREATE TABLE IF NOT EXISTS st_payment (
pay_id BIGINT PRIMARY KEY,
merchant_id TEXT NOT NULL,
net_amount BIGINT NOT NULL DEFAULT 0,
pay_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'SCHEDULED', -- SCHEDULED/HELD/PAID/CANCELLED
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 가맹점 정산 집계 (st_merch_settle) - 배치/ST_BYMERCH가 upsert
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS st_merch_settle (
merchant_id TEXT NOT NULL,
biz_date DATE NOT NULL,
txn_count BIGINT NOT NULL DEFAULT 0,
gross_amount BIGINT NOT NULL DEFAULT 0,
fee_amount BIGINT NOT NULL DEFAULT 0,
net_amount BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (merchant_id, biz_date)
);
-- 수수료 구간 요율 시드 (ST_BANDRATE / ST_RATEAPPLY 가 조회)
INSERT INTO st_fee_rate (fee_type, band_min, rate_bps) VALUES
('MDR', 0, 25),
('MDR', 10000000, 20),
('VAN', 0, 10),
('RELAY', 0, 5)
ON CONFLICT (fee_type, band_min) DO NOTHING;

67
db/schema.d/40-lg.sql Normal file
View file

@ -0,0 +1,67 @@
-- ---------------------------------------------------------------------------
-- lg 모듈 (원장/ledger) - 복식부기 전표/원장/계정잔액 스키마 (prefix 40)
-- Loaded after 00-base.sql by docker-entrypoint-initdb.d.
-- lg 서비스/배치가 채우는 전표(voucher) + 원장 라인(ledger_line) + 계정잔액 테이블.
-- ---------------------------------------------------------------------------
-- 전표 (voucher) - 하나의 회계 이벤트(매입전기/정산전기/조정/대체 등)의 헤더
CREATE SEQUENCE IF NOT EXISTS lg_voucher_seq;
CREATE TABLE IF NOT EXISTS lg_voucher (
voucher_id BIGINT PRIMARY KEY,
voucher_type TEXT NOT NULL, -- POST/DOUBLE/ADJ/REVERSE/TRANSFER/SETTLE/ACQ/FEE
ref_id BIGINT NOT NULL DEFAULT 0, -- 연계 매입/정산 ID
biz_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'D', -- D=작성, P=전기, M=대사, S=마감연계, X=취소
memo TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 원장 라인 (ledger_line) - 복식부기 차변(D)/대변(C) 개별 분개
CREATE SEQUENCE IF NOT EXISTS lg_line_seq;
CREATE TABLE IF NOT EXISTS lg_ledger_line (
line_id BIGINT PRIMARY KEY,
voucher_id BIGINT NOT NULL,
acct TEXT NOT NULL, -- 계정과목: RECV/MERCH_PAY/FEE_INC/CASH/ADJ ...
dc TEXT NOT NULL, -- D=차변(debit), C=대변(credit)
amount BIGINT NOT NULL,
merchant_id TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'posted', -- posted / closed
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_lg_line_voucher ON lg_ledger_line(voucher_id);
CREATE INDEX IF NOT EXISTS ix_lg_line_acct ON lg_ledger_line(acct, biz_date);
-- 계정잔액 (account balance) - 계정과목 x 영업일 누계
CREATE TABLE IF NOT EXISTS lg_account_bal (
acct TEXT NOT NULL,
biz_date DATE NOT NULL,
debit_total BIGINT NOT NULL DEFAULT 0,
credit_total BIGINT NOT NULL DEFAULT 0,
balance BIGINT NOT NULL DEFAULT 0, -- 차변 - 대변
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (acct, biz_date)
);
-- 가맹점 잔액 (merchant balance) - 가맹점 x 영업일 미지급/미수 누계
CREATE TABLE IF NOT EXISTS lg_merch_bal (
merchant_id TEXT NOT NULL,
biz_date DATE NOT NULL,
debit_total BIGINT NOT NULL DEFAULT 0,
credit_total BIGINT NOT NULL DEFAULT 0,
balance BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (merchant_id, biz_date)
);
-- 시산표 (trial balance) - 배치가 계정별로 채움
CREATE TABLE IF NOT EXISTS lg_trial_bal (
acct TEXT NOT NULL,
biz_date DATE NOT NULL,
debit_total BIGINT NOT NULL DEFAULT 0,
credit_total BIGINT NOT NULL DEFAULT 0,
diff BIGINT NOT NULL DEFAULT 0, -- 차변 - 대변 (0 이어야 정상)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (acct, biz_date)
);

51
db/schema.d/45-cl.sql Normal file
View file

@ -0,0 +1,51 @@
-- ---------------------------------------------------------------------------
-- cl 모듈 (마감/closing) - 일/월/분기/연 마감 로그 + 스냅샷 + 기간잠금 (prefix 45)
-- Loaded after 40-lg.sql by docker-entrypoint-initdb.d.
-- cl 서비스/배치가 채우는 마감이력(close_log) + 마감스냅샷(snapshot) 테이블.
-- ---------------------------------------------------------------------------
-- 마감 이력 (close_log) - 하나의 마감 실행(일/월/분기/연/중간)의 헤더
CREATE SEQUENCE IF NOT EXISTS cl_close_seq;
CREATE TABLE IF NOT EXISTS cl_close_log (
close_id BIGINT PRIMARY KEY,
close_type TEXT NOT NULL, -- DAILY/MONTHLY/QUARTER/YEAREND/MID
period_key TEXT NOT NULL, -- YYYY-MM-DD / YYYY-MM / YYYY-Qn / YYYY
biz_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'O', -- O=개시, C=마감완료, R=재마감, A=승인, L=잠금, X=취소
txn_count BIGINT NOT NULL DEFAULT 0,
total_amount BIGINT NOT NULL DEFAULT 0,
memo TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_cl_close_period ON cl_close_log(close_type, period_key);
-- 마감 스냅샷 (snapshot) - 마감 시점 가맹점별 집계 고정
CREATE SEQUENCE IF NOT EXISTS cl_snapshot_seq;
CREATE TABLE IF NOT EXISTS cl_snapshot (
snapshot_id BIGINT PRIMARY KEY,
close_id BIGINT NOT NULL,
period_key TEXT NOT NULL,
biz_date DATE NOT NULL,
merchant_id TEXT NOT NULL,
gross_amount BIGINT NOT NULL DEFAULT 0,
fee_amount BIGINT NOT NULL DEFAULT 0,
net_amount BIGINT NOT NULL DEFAULT 0,
txn_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_cl_snapshot_close ON cl_snapshot(close_id);
-- 기간 잠금 (period lock) - 마감된 기간의 추가 정정 차단
CREATE TABLE IF NOT EXISTS cl_period_lock (
period_key TEXT PRIMARY KEY,
locked BOOLEAN NOT NULL DEFAULT true,
locked_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 마감 시퀀스 관리 (close_type 별 마지막 순번)
CREATE TABLE IF NOT EXISTS cl_seq_reg (
close_type TEXT PRIMARY KEY,
last_seq BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

162
db/schema.d/50-mm.sql Normal file
View file

@ -0,0 +1,162 @@
-- mm 모듈 (마스터/master) 상세 스키마
-- 가맹점 확장원장 / 수수료율 / BIN / 공통코드 / 한도 / 발급사 / 단말 / MCC /
-- 공휴일 / 환율 마스터 + 이력. Loaded on first boot (docker-entrypoint-initdb.d).
-- ---------------------------------------------------------------------------
-- 가맹점 확장 마스터 (mm_merchant_ext) - base merchant를 보강하는 마스터 원장
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_merchant_ext (
merchant_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
biz_reg_no TEXT NOT NULL DEFAULT '', -- 사업자등록번호
grade TEXT NOT NULL DEFAULT 'C', -- 가맹점 등급 (S/A/B/C)
settle_acct TEXT NOT NULL DEFAULT '', -- 정산 계좌
mcc TEXT NOT NULL DEFAULT '0000', -- 업종코드
status TEXT NOT NULL DEFAULT 'ACTIVE',-- ACTIVE / HOLD / DELETED
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 가맹점 마스터 변경 이력
CREATE SEQUENCE IF NOT EXISTS mm_merch_hist_seq;
CREATE TABLE IF NOT EXISTS mm_merch_hist (
hist_id BIGINT PRIMARY KEY,
merchant_id TEXT NOT NULL,
action TEXT NOT NULL, -- REG / UPD / DEL / STATUS / GRADE / ACCT
before_val TEXT NOT NULL DEFAULT '',
after_val TEXT NOT NULL DEFAULT '',
memo TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 수수료율 마스터 (mm_fee_rate) + 이력
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_fee_rate (
merchant_id TEXT NOT NULL,
card_type TEXT NOT NULL DEFAULT 'CREDIT',-- CREDIT / CHECK / PREPAID
rate_bps BIGINT NOT NULL DEFAULT 25, -- basis points
eff_date DATE NOT NULL DEFAULT current_date,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (merchant_id, card_type)
);
CREATE SEQUENCE IF NOT EXISTS mm_feerate_hist_seq;
CREATE TABLE IF NOT EXISTS mm_fee_rate_hist (
hist_id BIGINT PRIMARY KEY,
merchant_id TEXT NOT NULL,
card_type TEXT NOT NULL,
rate_bps BIGINT NOT NULL,
eff_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- BIN 마스터 (mm_bin)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_bin (
bin TEXT PRIMARY KEY, -- 6-8 digit BIN
issuer_cd TEXT NOT NULL DEFAULT 'UNKNOWN',
card_brand TEXT NOT NULL DEFAULT 'LOCAL', -- VISA / MASTER / LOCAL
card_type TEXT NOT NULL DEFAULT 'CREDIT',
status TEXT NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 공통코드 마스터 (mm_code)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_code (
code_group TEXT NOT NULL,
code TEXT NOT NULL,
code_name TEXT NOT NULL DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (code_group, code)
);
-- ---------------------------------------------------------------------------
-- 가맹점 한도 마스터 (mm_limit)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_limit (
merchant_id TEXT PRIMARY KEY,
daily_limit BIGINT NOT NULL DEFAULT 100000000,
monthly_limit BIGINT NOT NULL DEFAULT 3000000000,
per_txn_limit BIGINT NOT NULL DEFAULT 10000000,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 발급사 마스터 (mm_issuer)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_issuer (
issuer_cd TEXT PRIMARY KEY,
issuer_name TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT 'KR',
status TEXT NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 단말 마스터 (mm_terminal)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_terminal (
terminal_id TEXT PRIMARY KEY,
merchant_id TEXT NOT NULL,
model TEXT NOT NULL DEFAULT 'STD',
status TEXT NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- MCC 업종 마스터 (mm_mcc)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_mcc (
mcc TEXT PRIMARY KEY,
mcc_name TEXT NOT NULL DEFAULT '',
risk_level INTEGER NOT NULL DEFAULT 1
);
-- ---------------------------------------------------------------------------
-- 공휴일 마스터 (mm_holiday)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_holiday (
holiday_date DATE PRIMARY KEY,
holiday_name TEXT NOT NULL DEFAULT '',
nation TEXT NOT NULL DEFAULT 'KR'
);
-- ---------------------------------------------------------------------------
-- 환율 마스터 (mm_fx_rate)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_fx_rate (
ccy TEXT PRIMARY KEY,
rate_bps BIGINT NOT NULL DEFAULT 100, -- KRW per unit, in bps of 100
base_date DATE NOT NULL DEFAULT current_date,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 등급 통계 (mm_grade_stat) - mm_stat_batch가 채움
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mm_grade_stat (
stat_date DATE NOT NULL,
grade TEXT NOT NULL,
merch_cnt BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (stat_date, grade)
);
-- Seed 몇 개 마스터 (테스트/데모용)
INSERT INTO mm_code (code_group, code, code_name, sort_order) VALUES
('CARD_TYPE', 'CREDIT', '신용카드', 1),
('CARD_TYPE', 'CHECK', '체크카드', 2),
('GRADE', 'S', '최우수', 1),
('GRADE', 'A', '우수', 2)
ON CONFLICT (code_group, code) DO NOTHING;
INSERT INTO mm_issuer (issuer_cd, issuer_name, country) VALUES
('BC', 'BC카드', 'KR'),
('SHC', '신한카드', 'KR')
ON CONFLICT (issuer_cd) DO NOTHING;

53
db/schema.d/55-vl.sql Normal file
View file

@ -0,0 +1,53 @@
-- vl 모듈 (정합성검증/validation) 상세 스키마
-- 검증 규칙 / 의심거래(suspect) / 검증 로그. Loaded on first boot.
-- ---------------------------------------------------------------------------
-- 검증 규칙 마스터 (vl_rule)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS vl_rule (
rule_code TEXT PRIMARY KEY,
rule_name TEXT NOT NULL DEFAULT '',
threshold BIGINT NOT NULL DEFAULT 0, -- 규칙별 임계값 (금액/건수 등)
severity TEXT NOT NULL DEFAULT 'WARN', -- INFO / WARN / ERROR / CRIT
status TEXT NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 의심거래 (vl_suspect) - 검증 실패/이상 거래 적재
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS vl_suspect_seq;
CREATE TABLE IF NOT EXISTS vl_suspect (
suspect_id BIGINT PRIMARY KEY,
purchase_id BIGINT NOT NULL DEFAULT 0,
rule_code TEXT NOT NULL,
severity TEXT NOT NULL DEFAULT 'WARN',
detail TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'OPEN', -- OPEN / CONFIRMED / CLOSED
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 검증 로그 (vl_check_log) - 모든 검증 서비스 호출 기록
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS vl_checklog_seq;
CREATE TABLE IF NOT EXISTS vl_check_log (
log_id BIGINT PRIMARY KEY,
check_type TEXT NOT NULL, -- 서비스명 (VL_SALES 등)
target_id BIGINT NOT NULL DEFAULT 0, -- 검증 대상 (purchase_id 등)
result_code BIGINT NOT NULL DEFAULT 0, -- 0=정상, >0=검증 실패코드
detail TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Seed 기본 검증 규칙
INSERT INTO vl_rule (rule_code, rule_name, threshold, severity) VALUES
('AMT_MAX', '최대금액 초과', 50000000, 'ERROR'),
('AMT_ZERO', '0원/음수 금액', 0, 'ERROR'),
('DUP_TXN', '중복 매입', 1, 'WARN'),
('LIMIT_OVR', '일한도 초과', 0, 'WARN'),
('LEDGER_MIS','원장 불일치', 0, 'CRIT')
ON CONFLICT (rule_code) DO NOTHING;

119
db/schema.d/60-mg.sql Normal file
View file

@ -0,0 +1,119 @@
-- mg 모듈 (전문게이트웨이 / message gateway) 스키마
-- ISO8583 전문 로그/라우팅/채번/채널세션/큐/통계 테이블.
-- Loaded into PostgreSQL on first boot (docker-entrypoint-initdb.d). Prefix 60.
-- ---------------------------------------------------------------------------
-- 전문 채번 (STAN / 전문 시퀀스) - MG_STAN 이 nextval 로 채번
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS mg_stan_seq; -- System Trace Audit Number
CREATE SEQUENCE IF NOT EXISTS mg_msg_seq; -- 전문 로그 PK
CREATE SEQUENCE IF NOT EXISTS mg_route_seq; -- 라우팅 룰 PK
CREATE SEQUENCE IF NOT EXISTS mg_queue_seq; -- 송수신 큐 PK
-- 이름있는 카운터 (세션 시퀀스 등 채번용 - MG_SESSION/MG_KEYEXCH)
CREATE TABLE IF NOT EXISTS mg_seq (
seq_name TEXT PRIMARY KEY,
seq_val BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 전문 로그 (mg_msg_log) - 수신/송신 전문의 원장
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mg_msg_log (
msg_id BIGINT PRIMARY KEY,
stan BIGINT NOT NULL, -- 전문 추적번호
mti TEXT NOT NULL, -- 전문유형(0200/0210/0800...)
channel TEXT NOT NULL, -- 채널ID
direction TEXT NOT NULL, -- IN / OUT
proc_code TEXT NOT NULL DEFAULT '000000', -- 처리코드
bitmap TEXT NOT NULL DEFAULT '', -- 비트맵(hex)
amount BIGINT NOT NULL DEFAULT 0,
pan TEXT NOT NULL DEFAULT '', -- (마스킹된) 카드번호
mac TEXT NOT NULL DEFAULT '', -- 메시지 인증코드
rc TEXT NOT NULL DEFAULT '00', -- 응답코드
raw_msg TEXT NOT NULL DEFAULT '', -- 원 전문
status TEXT NOT NULL DEFAULT 'R', -- R=수신, S=송신, A=ACK, N=NACK, C=확인
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mg_msg_log_stan ON mg_msg_log (stan);
CREATE INDEX IF NOT EXISTS ix_mg_msg_log_chan ON mg_msg_log (channel, biz_date);
-- ---------------------------------------------------------------------------
-- 라우팅 룰 (mg_route) - 채널/전문유형 -> 목적지 매핑
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mg_route (
route_id BIGINT PRIMARY KEY,
channel TEXT NOT NULL,
mti TEXT NOT NULL,
dest TEXT NOT NULL, -- 목적지 서비스/노드
priority INTEGER NOT NULL DEFAULT 100,
active BOOLEAN NOT NULL DEFAULT true,
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mg_route_lookup ON mg_route (channel, mti, active);
-- ---------------------------------------------------------------------------
-- 채널 (mg_channel) - 세션/사인온 상태
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mg_channel (
channel TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'DOWN', -- UP / DOWN / SIGNON / SIGNOFF
session_key TEXT NOT NULL DEFAULT '',
last_stan BIGINT NOT NULL DEFAULT 0,
signon_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 송수신 큐 (mg_queue) - 재전송 대상 관리
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mg_queue (
queue_id BIGINT PRIMARY KEY,
stan BIGINT NOT NULL,
channel TEXT NOT NULL,
mti TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'Q', -- Q=대기, S=송신, D=완료, F=실패
retry_cnt INTEGER NOT NULL DEFAULT 0,
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_mg_queue_status ON mg_queue (status, biz_date);
-- ---------------------------------------------------------------------------
-- 채널 일별 통계 (mg_stat) - mg_stat_batch 가 채움
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mg_stat (
channel TEXT NOT NULL,
biz_date DATE NOT NULL,
in_cnt BIGINT NOT NULL DEFAULT 0,
out_cnt BIGINT NOT NULL DEFAULT 0,
err_cnt BIGINT NOT NULL DEFAULT 0,
amount_sum BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (channel, biz_date)
);
-- ---------------------------------------------------------------------------
-- Seed: 채널 + 기본 라우팅 룰
-- ---------------------------------------------------------------------------
INSERT INTO mg_channel (channel, name, status) VALUES
('CH01', 'VAN 게이트웨이', 'DOWN'),
('CH02', '매입사 링크', 'DOWN'),
('CH03', '해외 네트워크', 'DOWN')
ON CONFLICT (channel) DO NOTHING;
INSERT INTO mg_route (route_id, channel, mti, dest, priority, active, biz_date) VALUES
(nextval('mg_route_seq'), 'CH01', '0200', 'ACQUIRE', 10, true, '2026-07-19'),
(nextval('mg_route_seq'), 'CH01', '0800', 'MG_ECHO', 20, true, '2026-07-19'),
(nextval('mg_route_seq'), 'CH02', '0200', 'ACQ_EDC', 10, true, '2026-07-19')
ON CONFLICT DO NOTHING;
INSERT INTO mg_seq (seq_name, seq_val) VALUES
('SESSION', 0), ('KEYVER', 0)
ON CONFLICT (seq_name) DO NOTHING;

112
db/schema.d/65-cm.sql Normal file
View file

@ -0,0 +1,112 @@
-- cm 모듈 (공통 / common) 스키마
-- 공통코드/파라미터/휴일/환율/채번/공통로그 + seed 데이터. Prefix 65.
-- Loaded into PostgreSQL on first boot (docker-entrypoint-initdb.d).
-- ---------------------------------------------------------------------------
-- 공통코드 (cm_code)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cm_code (
code_grp TEXT NOT NULL, -- 코드 그룹 (예: CCY, CHANNEL)
code TEXT NOT NULL, -- 코드 값
code_name TEXT NOT NULL, -- 코드 명
sort_ord INTEGER NOT NULL DEFAULT 0,
use_yn TEXT NOT NULL DEFAULT 'Y',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (code_grp, code)
);
-- ---------------------------------------------------------------------------
-- 공통 파라미터 (cm_param) - 시스템 운영 파라미터 (BIZ_DATE 포함)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cm_param (
param_key TEXT PRIMARY KEY,
param_val TEXT NOT NULL,
descr TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 휴일 (cm_holiday) - 영업일 계산에 사용
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cm_holiday (
holi_date DATE PRIMARY KEY,
holi_name TEXT NOT NULL,
nation TEXT NOT NULL DEFAULT 'KR'
);
-- ---------------------------------------------------------------------------
-- 환율 (cm_fx_rate) - 통화별 기준일 환율 (bps of 100 = KRW*100/unit)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cm_fx_rate (
ccy TEXT NOT NULL,
base_date DATE NOT NULL,
rate_bps BIGINT NOT NULL, -- KRW per unit * 100
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (ccy, base_date)
);
-- ---------------------------------------------------------------------------
-- 채번 (cm_seq) - 이름있는 공통 시퀀스 (CM_SEQ 서비스)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cm_seq (
seq_name TEXT PRIMARY KEY,
seq_val BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- 공통 로그 (cm_log) - CM_LOG 서비스 + cm_purge_batch 대상
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS cm_log_seq;
CREATE TABLE IF NOT EXISTS cm_log (
log_id BIGINT PRIMARY KEY,
svc_name TEXT NOT NULL DEFAULT '',
log_level TEXT NOT NULL DEFAULT 'I', -- I/W/E
msg TEXT NOT NULL DEFAULT '',
biz_date DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_cm_log_date ON cm_log (biz_date);
-- ---------------------------------------------------------------------------
-- Seed rows
-- ---------------------------------------------------------------------------
INSERT INTO cm_code (code_grp, code, code_name, sort_ord) VALUES
('CCY', 'KRW', '원화', 1),
('CCY', 'USD', '미국 달러', 2),
('CCY', 'JPY', '일본 엔', 3),
('CCY', 'EUR', '유로', 4),
('CHANNEL', 'POS', 'POS 단말', 1),
('CHANNEL', 'EDC', 'EDC 단말', 2),
('CHANNEL', 'EDI', 'EDI 청구', 3),
('CHANNEL', 'FOREIGN', '해외 매입', 4),
('STATUS', 'A', '접수', 1),
('STATUS', 'S', '정산완료', 2),
('STATUS', 'C', '취소', 3)
ON CONFLICT (code_grp, code) DO NOTHING;
INSERT INTO cm_param (param_key, param_val, descr) VALUES
('BIZ_DATE', '2026-07-19', '현재 영업일'),
('VAT_RATE', '10', '부가세율(%)'),
('WHT_BPS', '2200', '원천징수율(bps)'),
('LOG_KEEP_DAYS','90', '공통로그 보존일수'),
('SETTLE_CYCLE','1', '정산주기(일)')
ON CONFLICT (param_key) DO NOTHING;
INSERT INTO cm_holiday (holi_date, holi_name, nation) VALUES
('2026-01-01', '신정', 'KR'),
('2026-03-01', '삼일절', 'KR'),
('2026-05-05', '어린이날', 'KR'),
('2026-08-15', '광복절', 'KR'),
('2026-12-25', '성탄절', 'KR')
ON CONFLICT (holi_date) DO NOTHING;
INSERT INTO cm_fx_rate (ccy, base_date, rate_bps) VALUES
('USD', '2026-07-19', 135000), -- 1350.00 KRW
('JPY', '2026-07-19', 900), -- 9.00 KRW
('EUR', '2026-07-19', 147000) -- 1470.00 KRW
ON CONFLICT (ccy, base_date) DO NOTHING;
INSERT INTO cm_seq (seq_name, seq_val) VALUES
('BATCH', 0), ('SLIP', 0), ('TICKET', 0)
ON CONFLICT (seq_name) DO NOTHING;

View file

@ -28,8 +28,10 @@ services:
sysctls:
fs.mqueue.msg_max: "512"
fs.mqueue.msgsize_max: "65536"
# ~330 서비스 = 330+ POSIX 큐. 기본 queues_max(256) 초과 → 11 모듈서버 부팅 실패하므로 상향.
fs.mqueue.queues_max: "8192"
ulimits:
msgqueue: 536870912
msgqueue: 2147483648
nofile: 65536
volumes:
- ../app:/app