diff --git a/app/conf/app.ini b/app/conf/app.ini index 570e385..d3fbafb 100644 --- a/app/conf/app.ini +++ b/app/conf/app.ini @@ -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 diff --git a/app/conf/ndrxconfig.xml b/app/conf/ndrxconfig.xml index cdc13de..9809bae 100644 --- a/app/conf/ndrxconfig.xml +++ b/app/conf/ndrxconfig.xml @@ -35,17 +35,65 @@ 1 -e ${NDRX_APPHOME}/log/ac_svr.log -r - + 110 1 1 + -e ${NDRX_APPHOME}/log/au_svr.log -r + + + 120 + 1 + 1 + -e ${NDRX_APPHOME}/log/cl_svr.log -r + + + 130 + 1 + 1 + -e ${NDRX_APPHOME}/log/cm_svr.log -r + + + 140 + 1 + 1 + -e ${NDRX_APPHOME}/log/lg_svr.log -r + + + 150 + 1 + 1 + -e ${NDRX_APPHOME}/log/mg_svr.log -r + + + 160 + 1 + 1 + -e ${NDRX_APPHOME}/log/mm_svr.log -r + + + 170 + 1 + 1 + -e ${NDRX_APPHOME}/log/py_svr.log -r + + + 180 + 1 + 1 -e ${NDRX_APPHOME}/log/rc_svr.log -r - 120 + 190 1 1 -e ${NDRX_APPHOME}/log/st_svr.log -r + + 200 + 1 + 1 + -e ${NDRX_APPHOME}/log/vl_svr.log -r + diff --git a/app/src/au/au_svr.pgc b/app/src/au/au_svr.pgc new file mode 100644 index 0000000..2384ecd --- /dev/null +++ b/app/src/au/au_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/au/batch/au_fraud_scan_batch.pgc b/app/src/au/batch/au_fraud_scan_batch.pgc new file mode 100644 index 0000000..512403b --- /dev/null +++ b/app/src/au/batch/au_fraud_scan_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/au/batch/au_settle_reflect_batch.pgc b/app/src/au/batch/au_settle_reflect_batch.pgc new file mode 100644 index 0000000..9854d6a --- /dev/null +++ b/app/src/au/batch/au_settle_reflect_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/au/dbio/au_auth_dbio.pgc b/app/src/au/dbio/au_auth_dbio.pgc new file mode 100644 index 0000000..de3d95b --- /dev/null +++ b/app/src/au/dbio/au_auth_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/au/dbio/au_dbio.h b/app/src/au/dbio/au_dbio.h new file mode 100644 index 0000000..641fe68 --- /dev/null +++ b/app/src/au/dbio/au_dbio.h @@ -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 */ diff --git a/app/src/au/dbio/au_limit_dbio.pgc b/app/src/au/dbio/au_limit_dbio.pgc new file mode 100644 index 0000000..23bf0b5 --- /dev/null +++ b/app/src/au/dbio/au_limit_dbio.pgc @@ -0,0 +1,108 @@ +/* + * au_limit_dbio.pgc - 카드 한도(card_limit) + 승인 일집계 DB 접근 (libaudbio.a). + */ +#include +#include +#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; +} diff --git a/app/src/cl/batch/cl_daily_batch.pgc b/app/src/cl/batch/cl_daily_batch.pgc new file mode 100644 index 0000000..aadedc5 --- /dev/null +++ b/app/src/cl/batch/cl_daily_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/cl/batch/cl_snapshot_batch.pgc b/app/src/cl/batch/cl_snapshot_batch.pgc new file mode 100644 index 0000000..f2da5d6 --- /dev/null +++ b/app/src/cl/batch/cl_snapshot_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/cl/cl_svr.pgc b/app/src/cl/cl_svr.pgc new file mode 100644 index 0000000..6868cab --- /dev/null +++ b/app/src/cl/cl_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/cl/dbio/cl_close_dbio.pgc b/app/src/cl/dbio/cl_close_dbio.pgc new file mode 100644 index 0000000..79211a5 --- /dev/null +++ b/app/src/cl/dbio/cl_close_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/cl/dbio/cl_dbio.h b/app/src/cl/dbio/cl_dbio.h new file mode 100644 index 0000000..b8c9729 --- /dev/null +++ b/app/src/cl/dbio/cl_dbio.h @@ -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 */ diff --git a/app/src/cl/dbio/cl_snapshot_dbio.pgc b/app/src/cl/dbio/cl_snapshot_dbio.pgc new file mode 100644 index 0000000..4c2383f --- /dev/null +++ b/app/src/cl/dbio/cl_snapshot_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/cm/batch/cm_bizday_roll_batch.pgc b/app/src/cm/batch/cm_bizday_roll_batch.pgc new file mode 100644 index 0000000..ffc035c --- /dev/null +++ b/app/src/cm/batch/cm_bizday_roll_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/cm/batch/cm_purge_batch.pgc b/app/src/cm/batch/cm_purge_batch.pgc new file mode 100644 index 0000000..74a6f84 --- /dev/null +++ b/app/src/cm/batch/cm_purge_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/cm/cm_svr.pgc b/app/src/cm/cm_svr.pgc new file mode 100644 index 0000000..756b89d --- /dev/null +++ b/app/src/cm/cm_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/cm/dbio/cm_code_dbio.pgc b/app/src/cm/dbio/cm_code_dbio.pgc new file mode 100644 index 0000000..ded04ce --- /dev/null +++ b/app/src/cm/dbio/cm_code_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/cm/dbio/cm_dbio.h b/app/src/cm/dbio/cm_dbio.h new file mode 100644 index 0000000..e7db49c --- /dev/null +++ b/app/src/cm/dbio/cm_dbio.h @@ -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 */ diff --git a/app/src/cm/dbio/cm_param_dbio.pgc b/app/src/cm/dbio/cm_param_dbio.pgc new file mode 100644 index 0000000..2b62cff --- /dev/null +++ b/app/src/cm/dbio/cm_param_dbio.pgc @@ -0,0 +1,93 @@ +/* + * cm_param_dbio.pgc - 공통 파라미터/채번/공통로그 DB 접근 (part of libcmdbio.a). + */ +#include +#include +#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; +} diff --git a/app/src/lg/batch/lg_post_batch.pgc b/app/src/lg/batch/lg_post_batch.pgc new file mode 100644 index 0000000..aac2f1f --- /dev/null +++ b/app/src/lg/batch/lg_post_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/lg/batch/lg_trialbal_batch.pgc b/app/src/lg/batch/lg_trialbal_batch.pgc new file mode 100644 index 0000000..29563f0 --- /dev/null +++ b/app/src/lg/batch/lg_trialbal_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/lg/dbio/lg_balance_dbio.pgc b/app/src/lg/dbio/lg_balance_dbio.pgc new file mode 100644 index 0000000..5f631cb --- /dev/null +++ b/app/src/lg/dbio/lg_balance_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/lg/dbio/lg_dbio.h b/app/src/lg/dbio/lg_dbio.h new file mode 100644 index 0000000..ff8ca0e --- /dev/null +++ b/app/src/lg/dbio/lg_dbio.h @@ -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 */ diff --git a/app/src/lg/dbio/lg_ledger_dbio.pgc b/app/src/lg/dbio/lg_ledger_dbio.pgc new file mode 100644 index 0000000..e7ecb88 --- /dev/null +++ b/app/src/lg/dbio/lg_ledger_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/lg/lg_svr.pgc b/app/src/lg/lg_svr.pgc new file mode 100644 index 0000000..982561f --- /dev/null +++ b/app/src/lg/lg_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/mg/batch/mg_resend_batch.pgc b/app/src/mg/batch/mg_resend_batch.pgc new file mode 100644 index 0000000..9e48510 --- /dev/null +++ b/app/src/mg/batch/mg_resend_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/mg/batch/mg_stat_batch.pgc b/app/src/mg/batch/mg_stat_batch.pgc new file mode 100644 index 0000000..a22df5d --- /dev/null +++ b/app/src/mg/batch/mg_stat_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/mg/dbio/mg_dbio.h b/app/src/mg/dbio/mg_dbio.h new file mode 100644 index 0000000..8be9a1d --- /dev/null +++ b/app/src/mg/dbio/mg_dbio.h @@ -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 */ diff --git a/app/src/mg/dbio/mg_msg_dbio.pgc b/app/src/mg/dbio/mg_msg_dbio.pgc new file mode 100644 index 0000000..4257daf --- /dev/null +++ b/app/src/mg/dbio/mg_msg_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/mg/dbio/mg_route_dbio.pgc b/app/src/mg/dbio/mg_route_dbio.pgc new file mode 100644 index 0000000..751caa0 --- /dev/null +++ b/app/src/mg/dbio/mg_route_dbio.pgc @@ -0,0 +1,111 @@ +/* + * mg_route_dbio.pgc - 라우팅 룰 + 채널 세션 DB 접근 (part of libmgdbio.a). + */ +#include +#include +#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; +} diff --git a/app/src/mg/mg_svr.pgc b/app/src/mg/mg_svr.pgc new file mode 100644 index 0000000..624b959 --- /dev/null +++ b/app/src/mg/mg_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/mm/batch/mm_grade_recalc_batch.pgc b/app/src/mm/batch/mm_grade_recalc_batch.pgc new file mode 100644 index 0000000..fdb2ca5 --- /dev/null +++ b/app/src/mm/batch/mm_grade_recalc_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/mm/batch/mm_stat_batch.pgc b/app/src/mm/batch/mm_stat_batch.pgc new file mode 100644 index 0000000..9cef65b --- /dev/null +++ b/app/src/mm/batch/mm_stat_batch.pgc @@ -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 +#include +#include +#include + +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; +} diff --git a/app/src/mm/dbio/mm_dbio.h b/app/src/mm/dbio/mm_dbio.h new file mode 100644 index 0000000..22408d1 --- /dev/null +++ b/app/src/mm/dbio/mm_dbio.h @@ -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 */ diff --git a/app/src/mm/dbio/mm_feerate_dbio.pgc b/app/src/mm/dbio/mm_feerate_dbio.pgc new file mode 100644 index 0000000..6bb437a --- /dev/null +++ b/app/src/mm/dbio/mm_feerate_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/mm/dbio/mm_merch_dbio.pgc b/app/src/mm/dbio/mm_merch_dbio.pgc new file mode 100644 index 0000000..62ad0ed --- /dev/null +++ b/app/src/mm/dbio/mm_merch_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/mm/mm_svr.pgc b/app/src/mm/mm_svr.pgc new file mode 100644 index 0000000..12cd395 --- /dev/null +++ b/app/src/mm/mm_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/py/batch/py_filegen_batch.pgc b/app/src/py/batch/py_filegen_batch.pgc new file mode 100644 index 0000000..6791e6e --- /dev/null +++ b/app/src/py/batch/py_filegen_batch.pgc @@ -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_.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 +#include +#include +#include +#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; +} diff --git a/app/src/py/batch/py_result_batch.pgc b/app/src/py/batch/py_result_batch.pgc new file mode 100644 index 0000000..7e80007 --- /dev/null +++ b/app/src/py/batch/py_result_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/py/dbio/py_account_dbio.pgc b/app/src/py/dbio/py_account_dbio.pgc new file mode 100644 index 0000000..4e8eda3 --- /dev/null +++ b/app/src/py/dbio/py_account_dbio.pgc @@ -0,0 +1,44 @@ +/* + * py_account_dbio.pgc - 지급계좌(py_account) DB 접근 (libpydbio.a). + */ +#include +#include +#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; +} diff --git a/app/src/py/dbio/py_dbio.h b/app/src/py/dbio/py_dbio.h new file mode 100644 index 0000000..f39e87a --- /dev/null +++ b/app/src/py/dbio/py_dbio.h @@ -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 */ diff --git a/app/src/py/dbio/py_payout_dbio.pgc b/app/src/py/dbio/py_payout_dbio.pgc new file mode 100644 index 0000000..0858a21 --- /dev/null +++ b/app/src/py/dbio/py_payout_dbio.pgc @@ -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 +#include +#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; +} diff --git a/app/src/py/py_svr.pgc b/app/src/py/py_svr.pgc new file mode 100644 index 0000000..1882c67 --- /dev/null +++ b/app/src/py/py_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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: */ diff --git a/app/src/rc/batch/rc_diff_batch.pgc b/app/src/rc/batch/rc_diff_batch.pgc new file mode 100644 index 0000000..c0b90fe --- /dev/null +++ b/app/src/rc/batch/rc_diff_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/rc/batch/rc_match_batch.pgc b/app/src/rc/batch/rc_match_batch.pgc new file mode 100644 index 0000000..e26a38e --- /dev/null +++ b/app/src/rc/batch/rc_match_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/rc/dbio/rc_dbio.h b/app/src/rc/dbio/rc_dbio.h new file mode 100644 index 0000000..cb4a1f8 --- /dev/null +++ b/app/src/rc/dbio/rc_dbio.h @@ -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 */ diff --git a/app/src/rc/dbio/rc_ledger_dbio.pgc b/app/src/rc/dbio/rc_ledger_dbio.pgc new file mode 100644 index 0000000..83c307e --- /dev/null +++ b/app/src/rc/dbio/rc_ledger_dbio.pgc @@ -0,0 +1,68 @@ +/* + * rc_ledger_dbio.pgc - 대사 원장 연계 + 요약 upsert DB 접근 (librcdbio.a). + * ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음. + */ +#include +#include +#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; +} diff --git a/app/src/rc/dbio/rc_match_dbio.pgc b/app/src/rc/dbio/rc_match_dbio.pgc new file mode 100644 index 0000000..f4fd41d --- /dev/null +++ b/app/src/rc/dbio/rc_match_dbio.pgc @@ -0,0 +1,117 @@ +/* + * rc_match_dbio.pgc - 대사 로그/예외 DB 접근 (librcdbio.a). + * ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음. + */ +#include +#include +#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; +} diff --git a/app/src/rc/rc_svr.pgc b/app/src/rc/rc_svr.pgc index 8411d71..67923a9 100644 --- a/app/src/rc/rc_svr.pgc +++ b/app/src/rc/rc_svr.pgc @@ -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 #include @@ -13,22 +20,51 @@ #include #include #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); } diff --git a/app/src/st/batch/st_feecalc_batch.pgc b/app/src/st/batch/st_feecalc_batch.pgc new file mode 100644 index 0000000..6cf70f1 --- /dev/null +++ b/app/src/st/batch/st_feecalc_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/st/batch/st_settle_batch.pgc b/app/src/st/batch/st_settle_batch.pgc new file mode 100644 index 0000000..822b642 --- /dev/null +++ b/app/src/st/batch/st_settle_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/st/dbio/st_dbio.h b/app/src/st/dbio/st_dbio.h new file mode 100644 index 0000000..c7c8380 --- /dev/null +++ b/app/src/st/dbio/st_dbio.h @@ -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 */ diff --git a/app/src/st/dbio/st_fee_dbio.pgc b/app/src/st/dbio/st_fee_dbio.pgc new file mode 100644 index 0000000..35bfdda --- /dev/null +++ b/app/src/st/dbio/st_fee_dbio.pgc @@ -0,0 +1,51 @@ +/* + * st_fee_dbio.pgc - 수수료 구간요율 조회 + 정산 조정 DB 접근 (libstdbio.a). + * ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음. + */ +#include +#include +#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; +} diff --git a/app/src/st/dbio/st_settle_dbio.pgc b/app/src/st/dbio/st_settle_dbio.pgc new file mode 100644 index 0000000..f33fe94 --- /dev/null +++ b/app/src/st/dbio/st_settle_dbio.pgc @@ -0,0 +1,105 @@ +/* + * st_settle_dbio.pgc - 정산 상세/지급/가맹점 집계 DB 접근 (libstdbio.a). + * ECPG 함수; 호출자의 XA 브랜치에서 수행. EXEC SQL CONNECT 없음. + */ +#include +#include +#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; +} diff --git a/app/src/st/st_svr.pgc b/app/src/st/st_svr.pgc index 3f0da2c..c771e4b 100644 --- a/app/src/st/st_svr.pgc +++ b/app/src/st/st_svr.pgc @@ -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 #include @@ -13,7 +20,10 @@ #include #include #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: */ diff --git a/app/src/vl/batch/vl_anomaly_batch.pgc b/app/src/vl/batch/vl_anomaly_batch.pgc new file mode 100644 index 0000000..567205c --- /dev/null +++ b/app/src/vl/batch/vl_anomaly_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/vl/batch/vl_sales_batch.pgc b/app/src/vl/batch/vl_sales_batch.pgc new file mode 100644 index 0000000..a84c447 --- /dev/null +++ b/app/src/vl/batch/vl_sales_batch.pgc @@ -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 +#include +#include +#include +#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; +} diff --git a/app/src/vl/dbio/vl_check_dbio.pgc b/app/src/vl/dbio/vl_check_dbio.pgc new file mode 100644 index 0000000..dc9b88a --- /dev/null +++ b/app/src/vl/dbio/vl_check_dbio.pgc @@ -0,0 +1,92 @@ +/* + * vl_check_dbio.pgc - 의심거래 적재 + 검증 로그 기록 (libvldbio.a). + * ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT. + */ +#include +#include +#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; +} diff --git a/app/src/vl/dbio/vl_dbio.h b/app/src/vl/dbio/vl_dbio.h new file mode 100644 index 0000000..41bffd1 --- /dev/null +++ b/app/src/vl/dbio/vl_dbio.h @@ -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 */ diff --git a/app/src/vl/dbio/vl_rule_dbio.pgc b/app/src/vl/dbio/vl_rule_dbio.pgc new file mode 100644 index 0000000..bbdf348 --- /dev/null +++ b/app/src/vl/dbio/vl_rule_dbio.pgc @@ -0,0 +1,51 @@ +/* + * vl_rule_dbio.pgc - 검증 규칙 조회 + 등록 (libvldbio.a). + * ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT. + */ +#include +#include +#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; +} diff --git a/app/src/vl/vl_svr.pgc b/app/src/vl/vl_svr.pgc new file mode 100644 index 0000000..a3f0266 --- /dev/null +++ b/app/src/vl/vl_svr.pgc @@ -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 +#include +#include +#include +#include +#include +#include +#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 - 금액 유효성(0data; + 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: */ diff --git a/db/schema.d/20-au.sql b/db/schema.d/20-au.sql new file mode 100644 index 0000000..8956d7a --- /dev/null +++ b/db/schema.d/20-au.sql @@ -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; diff --git a/db/schema.d/25-py.sql b/db/schema.d/25-py.sql new file mode 100644 index 0000000..fad39f6 --- /dev/null +++ b/db/schema.d/25-py.sql @@ -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; diff --git a/db/schema.d/30-rc.sql b/db/schema.d/30-rc.sql new file mode 100644 index 0000000..14d71c5 --- /dev/null +++ b/db/schema.d/30-rc.sql @@ -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() +); diff --git a/db/schema.d/35-st.sql b/db/schema.d/35-st.sql new file mode 100644 index 0000000..026ddf8 --- /dev/null +++ b/db/schema.d/35-st.sql @@ -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; diff --git a/db/schema.d/40-lg.sql b/db/schema.d/40-lg.sql new file mode 100644 index 0000000..53b5115 --- /dev/null +++ b/db/schema.d/40-lg.sql @@ -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) +); diff --git a/db/schema.d/45-cl.sql b/db/schema.d/45-cl.sql new file mode 100644 index 0000000..0c4760e --- /dev/null +++ b/db/schema.d/45-cl.sql @@ -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() +); diff --git a/db/schema.d/50-mm.sql b/db/schema.d/50-mm.sql new file mode 100644 index 0000000..3178a94 --- /dev/null +++ b/db/schema.d/50-mm.sql @@ -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; diff --git a/db/schema.d/55-vl.sql b/db/schema.d/55-vl.sql new file mode 100644 index 0000000..e3b1b07 --- /dev/null +++ b/db/schema.d/55-vl.sql @@ -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; diff --git a/db/schema.d/60-mg.sql b/db/schema.d/60-mg.sql new file mode 100644 index 0000000..04951d5 --- /dev/null +++ b/db/schema.d/60-mg.sql @@ -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; diff --git a/db/schema.d/65-cm.sql b/db/schema.d/65-cm.sql new file mode 100644 index 0000000..fa23b20 --- /dev/null +++ b/db/schema.d/65-cm.sql @@ -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; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 106e5df..01656bb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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