규모확장 Wave A: 전 11모듈 svc 30→75·batch 22→35 + 전 모듈 운영데이터 시드

- svc 330→825 (모듈당 +45 고유 실업무: chargeback·재매입·심사워크플로·전문유형별·
  대사세분·정산시뮬·지급재처리·복식결산·마감워크플로·마스터심사·검증룰·전문라우팅 등)
- batch 242→385 (모듈당 +13), 카피북 헤더·디스패처 advertise 전 75 반영
- db/schema.d/95-module-seed.sql: 전 모듈 테이블 운영데이터(au 8.4k·st 15.9k·mg 16.9k·
  lg 14.6k·rc 7.7k·py 2.9k·vl 8.5k·cm 10.4k 등, 총 ~171k행) — base 거래와 정합
- 화면 매니페스트 825 재생성, 큐 메모리 튜닝(NDRX_MSGMAX 50→30)
- 검증: build DONE modules=11 batches=385, 12서버 runok, xadmin psc 828 AVAIL,
  매입체인 XA 커밋(status=S), 새서비스 시드데이터 스모크(ACQ_MERCHSUM/RC_STATS 실값),
  전 코퍼스 .pgc 클론 0, 헤드리스 스크린샷 확인

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyeongwoo-choi 2026-07-20 22:09:55 +00:00
parent 19acf4ed96
commit ea90bef264
1292 changed files with 53968 additions and 20 deletions

View file

@ -0,0 +1,54 @@
/*
* py_acctsync_batch.pgc - py 지급계좌 잔액 정합 점검 배치 (커서 + REVIEW 마킹, XA).
*
* 잔액이 음수로 뒤집힌 지급계좌를 커서로 찾아 REVIEW 상태로 마킹해
* 지급 경로에서 격리하고, 전체 최저 잔액도 리포트한다.
* host 변수는 전용 카피북 py_acctsync_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_acctsync_batch
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
long negatives = 0;
EXEC SQL INCLUDE py_acctsync_cpy;
(void)argc; (void)argv;
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 pas CURSOR FOR
SELECT account_no, merchant_id, balance FROM py_account
WHERE balance < 0 ORDER BY balance;
EXEC SQL OPEN pas;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pas INTO :c_acct, :c_merch, :c_bal;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pas; tpabort(0); return 1; }
negatives++;
printf(" [음수잔액] %s (%s) balance=%ld -> REVIEW\n", c_acct, c_merch, c_bal);
}
EXEC SQL CLOSE pas;
EXEC SQL UPDATE py_account SET status = 'REVIEW', updated_at = now()
WHERE balance < 0 AND status <> 'REVIEW';
if (sqlca.sqlcode < 0) { fprintf(stderr, "UPDATE FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
c_flagged = sqlca.sqlerrd[2];
EXEC SQL SELECT coalesce(min(balance),0) INTO :c_minbal FROM py_account;
if (sqlca.sqlcode < 0) { fprintf(stderr, "SELECT FAIL [%d]\n", sqlca.sqlcode); tpabort(0); return 1; }
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_acctsync_batch COMMIT: 음수계좌=%ld REVIEW신규=%ld 최저잔액=%ld\n",
negatives, c_flagged, c_minbal);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_acctsync_cpy.h - py_acctsync_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_acct[32];
char c_merch[64];
long c_bal, c_flagged, c_minbal;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,63 @@
/*
* py_advrepay_batch.pgc - py 선지급 분할 상환 배치 (커서 + adjust 기록, XA).
*
* 잔여 net 이 남은 ADVANCE 지급건을 커서로 훑어 상환률(bps)만큼 회수:
* py_adjust 음수 기록 + net 차감, 완납 시 마감(X) 전이까지 처리한다.
* host 변수는 전용 카피북 py_advrepay_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_advrepay_batch [YYYY-MM-DD] [RATE_BPS]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long repaid = 0, repaid_amt = 0, closed = 0;
EXEC SQL INCLUDE py_advrepay_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_bizdate)-1] = 0;
c_bps = (argc > 2) ? atol(argv[2]) : 1000; /* 기본 회당 10% 상환 */
if (c_bps <= 0 || c_bps > 10000) c_bps = 1000;
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(120, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE par CURSOR FOR
SELECT payout_id, net FROM py_payout
WHERE channel = 'ADVANCE' AND net > 0 AND status NOT IN ('C','V','X')
ORDER BY payout_id;
EXEC SQL OPEN par;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH par INTO :c_pid, :c_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE par; tpabort(0); return 1; }
c_repay = c_net * c_bps / 10000;
if (c_repay <= 0) c_repay = c_net; /* 소액 잔여는 완납 처리 */
c_newnet = c_net - c_repay;
c_adj = -c_repay;
EXEC SQL INSERT INTO py_adjust (adj_id, payout_id, delta, reason, biz_date)
VALUES (nextval('py_adjust_seq'), :c_pid, :c_adj, '선지급상환(배치)', :c_bizdate);
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE par; tpabort(0); return 1; }
EXEC SQL UPDATE py_payout
SET net = :c_newnet,
status = CASE WHEN :c_newnet <= 0 THEN 'X' ELSE status END,
updated_at = now()
WHERE payout_id = :c_pid;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE par; tpabort(0); return 1; }
repaid++; repaid_amt += c_repay;
if (c_newnet <= 0) closed++;
}
EXEC SQL CLOSE par;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_advrepay_batch COMMIT: bps=%ld 상환=%ld건/%ld원 완납마감=%ld건\n",
c_bps, repaid, repaid_amt, closed);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_advrepay_cpy.h - py_advrepay_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
long c_bps;
long c_pid, c_net, c_repay, c_newnet, c_adj;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,66 @@
/*
* py_bankfile_batch.pgc - py 은행별 지급파일 일괄 생성 배치 (커서 + 파일편성, XA).
*
* 당일 승인(A) 지급건의 은행코드를 DISTINCT 커서로 훑어, 은행마다 py_file
* 헤더를 만들고 해당 은행 건을 파일편성(F)+file_id 로 전이한다. ONE XA txn.
* host 변수는 전용 카피북 py_bankfile_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_bankfile_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long files = 0, recs = 0;
EXEC SQL INCLUDE py_bankfile_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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(120, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE pbf CURSOR FOR
SELECT DISTINCT a.bank_code
FROM py_payout p JOIN py_account a ON p.account_no = a.account_no
WHERE p.biz_date = :c_bizdate AND p.status = 'A'
ORDER BY a.bank_code;
EXEC SQL OPEN pbf;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pbf INTO :c_bank;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d]\n", sqlca.sqlcode); EXEC SQL CLOSE pbf; tpabort(0); return 1; }
EXEC SQL SELECT count(*), coalesce(sum(p.net),0) INTO :c_cnt, :c_sum
FROM py_payout p JOIN py_account a ON p.account_no = a.account_no
WHERE p.biz_date = :c_bizdate AND p.status = 'A' AND a.bank_code = :c_bank;
if (sqlca.sqlcode < 0 || c_cnt == 0) continue;
snprintf(c_fname, sizeof(c_fname), "PAYOUT_%s_%s.dat", c_bank, c_bizdate);
EXEC SQL SELECT nextval('py_file_seq') INTO :c_fid;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pbf; tpabort(0); return 1; }
EXEC SQL INSERT INTO py_file (file_id, biz_date, file_name, record_count, total_amount, status)
VALUES (:c_fid, :c_bizdate, :c_fname, :c_cnt, :c_sum, 'G');
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pbf; tpabort(0); return 1; }
EXEC SQL UPDATE py_payout SET status = 'F', file_id = :c_fid, updated_at = now()
FROM py_account a
WHERE py_payout.account_no = a.account_no AND py_payout.biz_date = :c_bizdate
AND py_payout.status = 'A' AND a.bank_code = :c_bank;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pbf; tpabort(0); return 1; }
files++; recs += c_cnt;
printf(" 은행 %s: 파일 %ld 생성 (%ld건 / %ld원)\n", c_bank, c_fid, c_cnt, c_sum);
}
EXEC SQL CLOSE pbf;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_bankfile_batch COMMIT: bizdate=%s files=%ld recs=%ld\n", bizdate, files, recs);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,7 @@
/* py_bankfile_cpy.h - py_bankfile_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
char c_bank[8];
char c_fname[64];
long c_fid, c_cnt, c_sum;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,59 @@
/*
* py_dormantstop_batch.pgc - py 휴면 가맹점 지급정지 배치 (NOT EXISTS 커서, XA).
*
* 최근 N일 지급 이력이 없는 정상 가맹점을 NOT EXISTS 커서로 뽑아, 가맹점별로
* 지급계좌를 DORMANT 로 내리고 잔여 요청(R)건을 '휴면정지' 보류시킨다.
* host 변수는 전용 카피북 py_dormantstop_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_dormantstop_batch [DAYS]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
long stopped = 0, total_accts = 0, total_held = 0;
EXEC SQL INCLUDE py_dormantstop_cpy;
c_days = (argc > 1) ? atol(argv[1]) : 30;
if (c_days <= 0) c_days = 30;
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(120, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE pds CURSOR FOR
SELECT m.merchant_id FROM merchant m
WHERE m.status = 'ACTIVE'
AND NOT EXISTS (SELECT 1 FROM py_payout p
WHERE p.merchant_id = m.merchant_id
AND p.created_at > now() - :c_days * interval '1 day')
ORDER BY m.merchant_id;
EXEC SQL OPEN pds;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pds INTO :c_merch;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pds; tpabort(0); return 1; }
EXEC SQL UPDATE py_account SET status = 'DORMANT', updated_at = now()
WHERE merchant_id = :c_merch AND status = 'ACTIVE';
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pds; tpabort(0); return 1; }
c_accts = sqlca.sqlerrd[2];
EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '휴면정지', updated_at = now()
WHERE merchant_id = :c_merch AND status = 'R';
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pds; tpabort(0); return 1; }
c_held = sqlca.sqlerrd[2];
stopped++; total_accts += c_accts; total_held += c_held;
printf(" [휴면정지] %s: 계좌 %ld개 정지, 지급 %ld건 보류\n", c_merch, c_accts, c_held);
}
EXEC SQL CLOSE pds;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_dormantstop_batch COMMIT: %ld일 기준 휴면=%ld곳 계좌=%ld 보류=%ld\n",
c_days, stopped, total_accts, total_held);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_dormantstop_cpy.h - py_dormantstop_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
long c_days;
char c_merch[64];
long c_accts, c_held;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,55 @@
/*
* py_failstat_batch.pgc - py 지급 실패사유 집계 리포트 배치 (GROUP BY 커서, XA).
*
* 당일 반려(J)건을 사유별 커서로 집계해 사유/건수/금액 표를 출력하고
* 최다 사유를 강조 리포트한다 (read-only 분석 배치).
* host 변수는 전용 카피북 py_failstat_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_failstat_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long groups = 0, fails = 0, amount = 0;
char top_reason[128] = "";
long top_cnt = 0;
EXEC SQL INCLUDE py_failstat_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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 pfs CURSOR FOR
SELECT coalesce(nullif(reject_reason,''),'(사유미기재)'), count(*), coalesce(sum(net),0)
FROM py_payout
WHERE biz_date = :c_bizdate AND status = 'J'
GROUP BY 1 ORDER BY 2 DESC, 1;
EXEC SQL OPEN pfs;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pfs INTO :c_reason, :c_cnt, :c_amt;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pfs; tpabort(0); return 1; }
printf(" [실패사유] %-24s %6ld건 %12ld원\n", c_reason, c_cnt, c_amt);
if (groups == 0) { strncpy(top_reason, c_reason, sizeof(top_reason)-1); top_cnt = c_cnt; }
groups++; fails += c_cnt; amount += c_amt;
}
EXEC SQL CLOSE pfs;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
if (groups > 0)
printf(">>> py_failstat_batch COMMIT: bizdate=%s 사유=%ld종 반려=%ld건/%ld원 최다='%s'(%ld건)\n",
bizdate, groups, fails, amount, top_reason, top_cnt);
else
printf(">>> py_failstat_batch COMMIT: bizdate=%s 반려 없음\n", bizdate);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_failstat_cpy.h - py_failstat_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
char c_reason[128];
long c_cnt, c_amt;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,50 @@
/*
* py_feewaive_batch.pgc - py 소액 지급 수수료 일괄 면제 배치 (INSERT..SELECT + UPDATE, XA).
*
* 당일 요청(R)건 중 지급액이 임계 이하인 소액건의 수수료를 전액 면제:
* 먼저 INSERT..SELECT 로 py_adjust 증적을 뜨고, 이어서 fee=0/net=amount 로
* 일괄 갱신한다 (증적 건수와 갱신 건수 일치 검증 출력).
* host 변수는 전용 카피북 py_feewaive_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_feewaive_batch [YYYY-MM-DD] [THRESHOLD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
EXEC SQL INCLUDE py_feewaive_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_bizdate)-1] = 0;
c_thr = (argc > 2) ? atol(argv[2]) : 30000; /* 기본 소액 기준 3만원 */
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 INSERT INTO py_adjust (adj_id, payout_id, delta, reason, biz_date)
SELECT nextval('py_adjust_seq'), payout_id, fee, '소액수수료면제', biz_date
FROM py_payout
WHERE biz_date = :c_bizdate AND status = 'R' AND fee > 0 AND amount <= :c_thr;
if (sqlca.sqlcode < 0) { fprintf(stderr, "INSERT FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
c_logged = sqlca.sqlerrd[2];
EXEC SQL UPDATE py_payout SET fee = 0, net = amount, updated_at = now()
WHERE biz_date = :c_bizdate AND status = 'R' AND fee > 0 AND amount <= :c_thr;
if (sqlca.sqlcode < 0) { fprintf(stderr, "UPDATE FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
c_waived = sqlca.sqlerrd[2];
if (c_logged != c_waived) {
fprintf(stderr, "py_feewaive_batch 증적/갱신 불일치 %ld != %ld -> ROLLBACK\n", c_logged, c_waived);
tpabort(0); return 1;
}
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_feewaive_batch COMMIT: bizdate=%s 기준=%ld원 면제=%ld건(증적 %ld)\n",
bizdate, c_thr, c_waived, c_logged);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,5 @@
/* py_feewaive_cpy.h - py_feewaive_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
long c_thr, c_logged, c_waived;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,48 @@
/*
* py_holdaging_batch.pgc - py 장기 보류 리포트 배치 (가맹점별 커서, XA).
*
* 보류(H) 상태로 N일 이상 경과한 건을 가맹점별 커서로 집계해 리포트를
* 출력한다 (조치 없는 read-only 점검 배치).
* host 변수는 전용 카피북 py_holdaging_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_holdaging_batch [DAYS]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
long merchants = 0, aged = 0, amount = 0;
EXEC SQL INCLUDE py_holdaging_cpy;
c_days = (argc > 1) ? atol(argv[1]) : 3;
if (c_days <= 0) c_days = 3;
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 pha CURSOR FOR
SELECT merchant_id, count(*), coalesce(sum(net),0)
FROM py_payout
WHERE status = 'H' AND updated_at < now() - :c_days * interval '1 day'
GROUP BY merchant_id ORDER BY 2 DESC;
EXEC SQL OPEN pha;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pha INTO :c_merch, :c_cnt, :c_amt;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pha; tpabort(0); return 1; }
printf(" [장기보류] %s: %ld건 %ld원\n", c_merch, c_cnt, c_amt);
merchants++; aged += c_cnt; amount += c_amt;
}
EXEC SQL CLOSE pha;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_holdaging_batch COMMIT: %ld일+ 보류 가맹점=%ld 건수=%ld 금액=%ld\n", c_days, merchants, aged, amount);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_holdaging_cpy.h - py_holdaging_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
long c_days;
char c_merch[64];
long c_cnt, c_amt;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,53 @@
/*
* py_limitguard_batch.pgc - py 지급한도 초과 자동 보류 배치 (HAVING 커서 + UPDATE, XA).
*
* 당일 확정성 지급(A/F/P) 합계가 일한도(merchant.daily_limit)를 넘긴 가맹점을
* GROUP BY ... HAVING 커서로 뽑아, 초과액과 함께 잔여 요청(R)건을 보류시킨다.
* host 변수는 전용 카피북 py_limitguard_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_limitguard_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long over_merchants = 0, held = 0;
EXEC SQL INCLUDE py_limitguard_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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(90, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE plg CURSOR FOR
SELECT m.merchant_id, coalesce(sum(p.net),0) - m.daily_limit
FROM py_payout p JOIN merchant m ON p.merchant_id = m.merchant_id
WHERE p.biz_date = :c_bizdate AND p.status IN ('A','F','P')
GROUP BY m.merchant_id, m.daily_limit
HAVING coalesce(sum(p.net),0) > m.daily_limit;
EXEC SQL OPEN plg;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH plg INTO :c_merch, :c_over;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE plg; tpabort(0); return 1; }
over_merchants++;
EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '한도초과', updated_at = now()
WHERE merchant_id = :c_merch AND biz_date = :c_bizdate AND status = 'R';
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE plg; tpabort(0); return 1; }
held += sqlca.sqlerrd[2];
printf(" [한도초과] %s: 초과액=%ld 보류=%ld건\n", c_merch, c_over, sqlca.sqlerrd[2]);
}
EXEC SQL CLOSE plg;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_limitguard_batch COMMIT: bizdate=%s 초과가맹점=%ld 보류=%ld건\n", bizdate, over_merchants, held);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_limitguard_cpy.h - py_limitguard_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
char c_merch[64];
long c_over, c_limit;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,60 @@
/*
* py_mergesmall_batch.pgc - py 분할건 합산 정리 배치 (HAVING 커서 + 재생성, XA).
*
* 당일 요청(R) SPLIT 건이 2건 이상인 가맹점을 커서로 뽑아 가맹점마다
* 원건들을 취소(C) 표시하고 합산 1건(BULK)으로 재생성한다 (이체 건수 절감).
* host 변수는 전용 카피북 py_mergesmall_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_mergesmall_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long merchants = 0, merged = 0;
EXEC SQL INCLUDE py_mergesmall_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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(120, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE pms CURSOR FOR
SELECT merchant_id, count(*), coalesce(sum(amount),0), coalesce(sum(fee),0),
coalesce(sum(net),0), coalesce(min(account_no),'')
FROM py_payout
WHERE biz_date = :c_bizdate AND status = 'R' AND channel = 'SPLIT'
GROUP BY merchant_id HAVING count(*) >= 2
ORDER BY merchant_id;
EXEC SQL OPEN pms;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pms INTO :c_merch, :c_cnt, :c_amt, :c_fee, :c_net, :c_acct;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pms; tpabort(0); return 1; }
EXEC SQL UPDATE py_payout SET status = 'C', reject_reason = '분할합산', updated_at = now()
WHERE merchant_id = :c_merch AND biz_date = :c_bizdate
AND status = 'R' AND channel = 'SPLIT';
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pms; tpabort(0); return 1; }
EXEC SQL SELECT nextval('py_payout_seq') INTO :c_newid;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pms; tpabort(0); return 1; }
EXEC SQL INSERT INTO py_payout (payout_id, merchant_id, account_no, amount, fee, net,
status, channel, biz_date)
VALUES (:c_newid, :c_merch, :c_acct, :c_amt, :c_fee, :c_net, 'R', 'BULK', :c_bizdate);
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pms; tpabort(0); return 1; }
merchants++; merged += c_cnt;
printf(" [분할합산] %s: %ld건 -> BULK id=%ld (net=%ld)\n", c_merch, c_cnt, c_newid, c_net);
}
EXEC SQL CLOSE pms;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_mergesmall_batch COMMIT: bizdate=%s 가맹점=%ld 통합원건=%ld\n", bizdate, merchants, merged);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,7 @@
/* py_mergesmall_cpy.h - py_mergesmall_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
char c_merch[64];
char c_acct[32];
long c_cnt, c_amt, c_fee, c_net, c_newid;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,47 @@
/*
* py_notify_batch.pgc - py 지급완료 통지 발송 시뮬 배치 (읽기 커서, XA).
*
* 당일 지급완료(P) 건을 커서로 훑어 가맹점별 SMS 통지 발송을 stdout 으로
* 시뮬레이션한다 (실발송 없음). 발송 건수/총액 리포트.
* host 변수는 전용 카피북 py_notify_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_notify_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long sent = 0, total = 0;
EXEC SQL INCLUDE py_notify_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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 pnt CURSOR FOR
SELECT payout_id, merchant_id, net FROM py_payout
WHERE biz_date = :c_bizdate AND status = 'P'
ORDER BY merchant_id, payout_id;
EXEC SQL OPEN pnt;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pnt INTO :c_pid, :c_merch, :c_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pnt; tpabort(0); return 1; }
printf(" [SMS시뮬] %s님 지급 %ld원 완료 (payout=%ld)\n", c_merch, c_net, c_pid);
sent++; total += c_net;
}
EXEC SQL CLOSE pnt;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_notify_batch COMMIT: bizdate=%s 통지=%ld건 총액=%ld\n", bizdate, sent, total);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,6 @@
/* py_notify_cpy.h - py_notify_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
char c_merch[64];
long c_pid, c_net;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,39 @@
/*
* py_schedexpire_batch.pgc - py 기한경과 지급예약 만료 배치 (DELETE + 잔여 점검, XA).
*
* 기준일 이전(run_date < 기준일)인데 아직 미실행(S)인 예약을 DELETE 로
* 만료시키고, 남은 유효 예약 건수를 함께 리포트한다.
* host 변수는 전용 카피북 py_schedexpire_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_schedexpire_batch [YYYY-MM-DD(기준일)]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *today = (argc > 1) ? argv[1] : "2026-07-19";
EXEC SQL INCLUDE py_schedexpire_cpy;
strncpy(c_today, today, sizeof(c_today)-1); c_today[sizeof(c_today)-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 DELETE FROM py_schedule
WHERE status = 'S' AND run_date < CAST(:c_today AS date);
if (sqlca.sqlcode < 0) { fprintf(stderr, "DELETE FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
c_expired = sqlca.sqlerrd[2];
EXEC SQL SELECT count(*) INTO :c_remain FROM py_schedule WHERE status = 'S';
if (sqlca.sqlcode < 0) { fprintf(stderr, "SELECT FAIL [%d]\n", sqlca.sqlcode); tpabort(0); return 1; }
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_schedexpire_batch COMMIT: 기준=%s 만료삭제=%ld건 잔여예약=%ld건\n", today, c_expired, c_remain);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,5 @@
/* py_schedexpire_cpy.h - py_schedexpire_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_today[16];
long c_expired, c_remain;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,43 @@
/*
* py_seizescan_batch.pgc - py 압류계좌 지급 차단 스캔 배치 (UPDATE FROM, XA).
*
* 압류(SEIZED) 계좌로 향하는 당일 미지급(R/A) 건을 UPDATE ... FROM 조인
* 한 방으로 '압류차단' 보류(H) 전이하고, 압류 계좌 수도 함께 리포트.
* host 변수는 전용 카피북 py_seizescan_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_seizescan_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
EXEC SQL INCLUDE py_seizescan_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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 SELECT count(*) INTO :c_accts FROM py_account WHERE status = 'SEIZED';
if (sqlca.sqlcode < 0) { fprintf(stderr, "SELECT FAIL [%d]\n", sqlca.sqlcode); tpabort(0); return 1; }
EXEC SQL UPDATE py_payout SET status = 'H', reject_reason = '압류차단', updated_at = now()
FROM py_account a
WHERE py_payout.account_no = a.account_no
AND py_payout.biz_date = :c_bizdate
AND py_payout.status IN ('R','A')
AND a.status = 'SEIZED';
if (sqlca.sqlcode < 0) { fprintf(stderr, "UPDATE FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
c_blocked = sqlca.sqlerrd[2];
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_seizescan_batch COMMIT: bizdate=%s 압류계좌=%ld 차단보류=%ld건\n", bizdate, c_accts, c_blocked);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,5 @@
/* py_seizescan_cpy.h - py_seizescan_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
long c_blocked, c_accts;
EXEC SQL END DECLARE SECTION;

View file

@ -0,0 +1,52 @@
/*
* py_xferrecon_batch.pgc - py 이체결과 대사 보정 배치 (안티조인 커서 + INSERT, XA).
*
* 당일 지급완료(P)인데 XFER 기표가 없는 건을 NOT EXISTS 커서로 찾아
* py_ledger 에 XFERFIX 보정 기표를 채워 원장을 맞춘다.
* host 변수는 전용 카피북 py_xferrecon_cpy.h 를 EXEC SQL INCLUDE.
* Usage: py_xferrecon_batch [YYYY-MM-DD]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <atmi.h>
int main(int argc, char **argv)
{
const char *bizdate = (argc > 1) ? argv[1] : "2026-07-19";
long fixed = 0, fixed_amt = 0;
EXEC SQL INCLUDE py_xferrecon_cpy;
strncpy(c_bizdate, bizdate, sizeof(c_bizdate)-1); c_bizdate[sizeof(c_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(90, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; }
EXEC SQL DECLARE pxr CURSOR FOR
SELECT p.payout_id, p.net FROM py_payout p
WHERE p.biz_date = :c_bizdate AND p.status = 'P'
AND NOT EXISTS (SELECT 1 FROM py_ledger l
WHERE l.payout_id = p.payout_id AND l.entry_type = 'XFER')
ORDER BY p.payout_id;
EXEC SQL OPEN pxr;
if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; }
for (;;) {
EXEC SQL FETCH pxr INTO :c_pid, :c_net;
if (sqlca.sqlcode == 100) break;
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pxr; tpabort(0); return 1; }
EXEC SQL INSERT INTO py_ledger (ledger_id, payout_id, entry_type, amount, biz_date)
VALUES (nextval('py_ledger_seq'), :c_pid, 'XFERFIX', :c_net, :c_bizdate);
if (sqlca.sqlcode < 0) { EXEC SQL CLOSE pxr; tpabort(0); return 1; }
fixed++; fixed_amt += c_net;
printf(" [대사보정] payout=%ld XFERFIX %ld원 기표\n", c_pid, c_net);
}
EXEC SQL CLOSE pxr;
if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; }
printf(">>> py_xferrecon_batch COMMIT: bizdate=%s 보정기표=%ld건 %ld원\n", bizdate, fixed, fixed_amt);
tpclose(); tpterm();
return 0;
}

View file

@ -0,0 +1,5 @@
/* py_xferrecon_cpy.h - py_xferrecon_batch 전용 카피북 (host 변수 선언). */
EXEC SQL BEGIN DECLARE SECTION;
char c_bizdate[16];
long c_pid, c_net;
EXEC SQL END DECLARE SECTION;