diff --git a/.gitignore b/.gitignore index eca07db..3d22648 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ # build artifacts *.o *.so +*.a build/ bin/acq* +# per-module build output (static libs + ecpg-generated .c/.o) +app/lib/ +app/obj/ # ecpg-generated C from .pgc (regenerated at build) **/gen/*.c # Enduro/X runtime state diff --git a/app/build.sh b/app/build.sh new file mode 100644 index 0000000..76d6497 --- /dev/null +++ b/app/build.sh @@ -0,0 +1,188 @@ +#!/bin/bash +## +## build.sh - REUSABLE build + Enduro/X config generator for acquire-core-x. +## +## Discovers every module under app/src//, ecpg-precompiles its .pgc, builds +## - libacqcommon.a (from app/src/common/*.c) [once] +## - libdbio.a (from app/src//dbio/*.pgc) [per module] +## - _svr (from app/src//_svr.pgc) [per module] +## - batches (from app/src//batch/*.pgc) [per module] +## - standalone clients (from app/src/clients/*.c) +## then GENERATES app/conf/ndrxconfig.xml with one entry per _svr. +## +## Adding a new module later = drop app/src//{_svr.pgc,dbio,batch} and +## re-run build.sh. No hand-editing of config or this script is required: modules, +## servers, dbio libs, batches, and srvids are all DISCOVERED. +## +set -euo pipefail + +SRC=/app/src +BIN=/app/bin +LIB=/app/lib +OBJ=/app/obj +UBF=/app/ubftab +CONF=/app/conf +EXINC=/usr/local/include +PGINC="$(pg_config --includedir)" +PGLIB="$(pg_config --libdir)" + +mkdir -p "$BIN" "$LIB" "$OBJ" + +# Includes for every compile (CFLAGS is honored by buildserver/buildclient and +# by our direct gcc calls). Add every module's dbio dir so headers resolve. +CFLAGS="-I$UBF -I$EXINC -I$PGINC -I$SRC/common" +for d in "$SRC"/*/dbio; do [ -d "$d" ] && CFLAGS="$CFLAGS -I$d"; done +export CFLAGS +PGLINK="-L$PGLIB -lecpg -lpq" + +log() { echo "[build] $*"; } + +# --------------------------------------------------------------------------- +# discover modules = dirs app/src// that contain _svr.pgc +# --------------------------------------------------------------------------- +MODS=() +for d in "$SRC"/*/; do + m="$(basename "$d")" + [ -f "$d/${m}_svr.pgc" ] && MODS+=("$m") +done +IFS=$'\n' MODS=($(printf '%s\n' "${MODS[@]}" | sort)); unset IFS +log "modules discovered: ${MODS[*]}" + +# --------------------------------------------------------------------------- +# 0. UBF field header +# --------------------------------------------------------------------------- +log "mkfldhdr acq.fd" +( cd "$UBF" && FIELDTBLS=acq.fd FLDTBLDIR="$UBF" mkfldhdr acq.fd >/dev/null ) + +# ecpg precompile helper: $1 = .pgc path -> echoes generated .c path +ecpg_gen() { + local pgc="$1" base out + base="$(basename "${pgc%.pgc}")" + out="$OBJ/${base}.c" + ecpg -I"$UBF" -I"$(dirname "$pgc")" -o "$out" "$pgc" + echo "$out" +} + +# --------------------------------------------------------------------------- +# 1. common lib -> libacqcommon.a +# --------------------------------------------------------------------------- +log "libacqcommon.a" +COMMON_OBJS=() +for c in "$SRC"/common/*.c; do + o="$OBJ/$(basename "${c%.c}").o" + gcc -c $CFLAGS "$c" -o "$o" + COMMON_OBJS+=("$o") +done +ar rcs "$LIB/libacqcommon.a" "${COMMON_OBJS[@]}" +NSVR=0; NDBIO=0; NBATCH=0 + +# --------------------------------------------------------------------------- +# 2..4 per module: dbio lib, server, batches +# --------------------------------------------------------------------------- +for m in "${MODS[@]}"; do + MDIR="$SRC/$m" + DBIO_LIB="" + + # -- dbio static lib (optional) -- + if compgen -G "$MDIR/dbio/*.pgc" >/dev/null; then + log "lib${m}dbio.a" + DB_OBJS=() + for pgc in "$MDIR"/dbio/*.pgc; do + c="$(ecpg_gen "$pgc")" + o="${c%.c}.o" + gcc -c $CFLAGS "$c" -o "$o" + DB_OBJS+=("$o") + done + ar rcs "$LIB/lib${m}dbio.a" "${DB_OBJS[@]}" + DBIO_LIB="$LIB/lib${m}dbio.a" + NDBIO=$((NDBIO+1)) + fi + + # -- module server _svr -- + log "${m}_svr" + SVR_C="$(ecpg_gen "$MDIR/${m}_svr.pgc")" + buildserver -o "$BIN/${m}_svr" -f "$SVR_C" \ + -a "$DBIO_LIB $LIB/libacqcommon.a $PGLINK" + NSVR=$((NSVR+1)) + + # -- batches (optional): each *.pgc -> its own client executable -- + if compgen -G "$MDIR/batch/*.pgc" >/dev/null; then + for pgc in "$MDIR"/batch/*.pgc; do + name="$(basename "${pgc%.pgc}")" + log "batch $name" + BC="$(ecpg_gen "$pgc")" + buildclient -o "$BIN/$name" -f "$BC" \ + -a "$DBIO_LIB $LIB/libacqcommon.a $PGLINK" + NBATCH=$((NBATCH+1)) + done + fi +done + +# --------------------------------------------------------------------------- +# 5. standalone clients (plain C, no EXEC SQL) +# --------------------------------------------------------------------------- +NCLI=0 +for c in "$SRC"/clients/*.c; do + name="$(basename "${c%.c}")" + log "client $name" + buildclient -o "$BIN/$name" -f "$c" + NCLI=$((NCLI+1)) +done + +# --------------------------------------------------------------------------- +# 6. GENERATE ndrxconfig.xml (tmsrv fixed at srvid 40; module servers 100 step 10) +# --------------------------------------------------------------------------- +log "generating ndrxconfig.xml for servers: ${MODS[*]/%/_svr}" +gen_servers() { + local srvid=100 + for m in "${MODS[@]}"; do + cat < + ${srvid} + 1 + 1 + -e \${NDRX_APPHOME}/log/${m}_svr.log -r + +EOF + srvid=$((srvid+10)) + done +} +cat > "$CONF/ndrxconfig.xml" < + + + + 1 + 5 + 1 + 1 + 5 + 20 + Y + + + 1 + 1 + 1 + 20 + 100 + 800 + 30 + 1 + + + + + 40 + 1 + 1 + -e \${NDRX_APPHOME}/log/tmsrv-rm1.log -r -- -t1 -l\${NDRX_APPHOME}/tmlogs/rm1 -m10 + +$(gen_servers) + + +EOF + +log "DONE: modules=${#MODS[@]} servers=$NSVR dbio_libs=$NDBIO batches=$NBATCH clients=$NCLI" +echo "[build] server binaries: ${MODS[*]/%/_svr}" diff --git a/app/conf/ndrxconfig.xml b/app/conf/ndrxconfig.xml index 0384a8c..cdc13de 100644 --- a/app/conf/ndrxconfig.xml +++ b/app/conf/ndrxconfig.xml @@ -1,4 +1,6 @@ + 1 @@ -20,36 +22,30 @@ 1 - + 40 1 1 -e ${NDRX_APPHOME}/log/tmsrv-rm1.log -r -- -t1 -l${NDRX_APPHOME}/tmlogs/rm1 -m10 - - - + 100 1 1 - -e ${NDRX_APPHOME}/log/acqacquire.log -r + -e ${NDRX_APPHOME}/log/ac_svr.log -r - + 110 1 1 - -e ${NDRX_APPHOME}/log/acqreconcile.log -r + -e ${NDRX_APPHOME}/log/rc_svr.log -r - + 120 1 1 - -e ${NDRX_APPHOME}/log/acqsettle.log -r + -e ${NDRX_APPHOME}/log/st_svr.log -r diff --git a/app/entrypoint.sh b/app/entrypoint.sh index 80be350..f35cd03 100755 --- a/app/entrypoint.sh +++ b/app/entrypoint.sh @@ -1,18 +1,18 @@ #!/bin/bash ## -## Build + boot the Enduro/X acquiring slice, then stay alive. -## Runs inside the app container (image acquire-x/endurox:7.0.12). +## Build (via the reusable build.sh) + boot the Enduro/X acquiring stack, then +## stay alive. Runs inside the app container (image acquire-x/endurox:7.0.12). ## set -e . /app/conf/setapp.sh echo "==================================================================" -echo " acquire-core-x : build + boot" +echo " acquire-core-x : build + boot (module-server pattern)" echo "==================================================================" # runtime dirs -mkdir -p /app/log /app/tmp /app/tmlogs/rm1 /app/bin +mkdir -p /app/log /app/tmp /app/tmlogs/rm1 /app/bin /app/lib /app/obj rm -f /app/log/*.log 2>/dev/null || true # ensure posix mqueue is available (compose provides the sysctls/ulimits) @@ -22,40 +22,13 @@ if [ ! -d /dev/mqueue ]; then fi # --------------------------------------------------------------------------- -# 1. UBF field header from the field table +# Build everything + generate ndrxconfig.xml (discovers all modules) # --------------------------------------------------------------------------- -echo "--- mkfldhdr acq.fd ---" -cd /app/ubftab -FIELDTBLS=acq.fd FLDTBLDIR=/app/ubftab mkfldhdr acq.fd -ls -l /app/ubftab/acq.fd.h - -# --------------------------------------------------------------------------- -# 2. ECPG precompile + buildserver each .pgc (XA switch loaded via env, so -# buildserver needs no -r; we just link ecpg + pq). buildclient the driver. -# --------------------------------------------------------------------------- -export CFLAGS="-I/app/ubftab -I/app/src -I$(pg_config --includedir)" -PGLIB="-L$(pg_config --libdir) -lecpg -lpq" - -cd /app/src -build_svc () { - local src="$1" bin="$2" - echo "--- ecpg $src ---" - ecpg -I/app/ubftab -o "/app/src/${src%.pgc}.c" "/app/src/$src" - echo "--- buildserver -> $bin ---" - buildserver -o "/app/bin/$bin" -f "/app/src/${src%.pgc}.c" -a "$PGLIB" -} - -build_svc acquire.pgc acqacquire -build_svc reconcile.pgc acqreconcile -build_svc settle.pgc acqsettle - -echo "--- buildclient -> acqdrv ---" -buildclient -o /app/bin/acqdrv -f /app/src/acqdrv.c - +bash /app/build.sh ls -l /app/bin # --------------------------------------------------------------------------- -# 3. Boot Enduro/X +# Boot Enduro/X # --------------------------------------------------------------------------- echo "--- xadmin down/start ---" xadmin down -y 2>/dev/null || true @@ -67,7 +40,7 @@ echo "--- xadmin ppm (processes) ---" xadmin ppm echo "==================================================================" -echo " acquire-core-x : UP. Run the driver with:" +echo " acquire-core-x : UP. Run the 매입 chain driver with:" echo " docker compose -f docker/docker-compose.yml exec app /app/run-driver.sh" echo "==================================================================" diff --git a/app/run-driver.sh b/app/run-driver.sh index 034b46c..9d6af68 100755 --- a/app/run-driver.sh +++ b/app/run-driver.sh @@ -1,16 +1,29 @@ #!/bin/bash -## Run the acquiring driver under a real global XA transaction. -## ./run-driver.sh -> one COMMIT run + one ROLLBACK run (demo) -## ./run-driver.sh [abort] -> single custom run +## Run the acquiring drivers under real global XA transactions. +## ./run-driver.sh -> demo: COMMIT chain, ROLLBACK chain, ops, batches +## ./run-driver.sh [abort] -> single custom chain run . /app/conf/setapp.sh if [ "$#" -ge 1 ]; then exec /app/bin/acqdrv "$@" fi -echo "===== COMMIT run (ACQUIRE -> RECONCILE -> SETTLE, tpcommit) =====" +echo "===== 1) 매입 체인 COMMIT (ACQUIRE -> RECONCILE -> SETTLE, tpcommit) =====" /app/bin/acqdrv M0001 1000000 2026-07-19 echo -echo "===== ROLLBACK run (same chain, tpabort) =====" +echo "===== 2) 매입 체인 ROLLBACK (동일 체인, tpabort) =====" /app/bin/acqdrv M0002 500000 2026-07-19 abort + +echo +echo "===== 3) ac 온라인 서비스 ops (DUPCHK/FEEADJ/INSTALL/STATUS, tpcommit) =====" +LASTPID=$(PGPASSWORD=acq psql -h db -U acq -d acq -tAc "SELECT max(purchase_id) FROM purchase") +/app/bin/ac_ops "${LASTPID:-1}" M0001 1000000 2026-07-19 + +echo +echo "===== 4) ac 배치: 가맹점 일집계 (커서 + XA) =====" +/app/bin/ac_merchsum_batch 2026-07-19 + +echo +echo "===== 5) ac 배치: 원천징수 (커서 + XA) =====" +/app/bin/ac_wht_batch 2026-07-19 diff --git a/app/src/ac/ac_svr.pgc b/app/src/ac/ac_svr.pgc new file mode 100644 index 0000000..02d4e33 --- /dev/null +++ b/app/src/ac/ac_svr.pgc @@ -0,0 +1,687 @@ +/* + * ac_svr.pgc - ac (매입) MODULE SERVER. + * + * ONE binary that tpadvertise()s ALL ~30 online ac 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 libacdbio.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 (see the + * ACQUIRE->RECONCILE->SETTLE chain, which crosses ac_svr/rc_svr/st_svr). + */ +#include +#include +#include +#include +#include +#include +#include +#include "acq.fd.h" +#include "ac_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 ACQUIRE(TPSVCINFO *p); void ACQ_DDC(TPSVCINFO *p); +void ACQ_EDI(TPSVCINFO *p); void ACQ_EDC(TPSVCINFO *p); +void ACQ_CANCEL(TPSVCINFO *p); void ACQ_CORRECT(TPSVCINFO *p); +void ACQ_PARTIAL(TPSVCINFO *p); void ACQ_INSTALL(TPSVCINFO *p); +void ACQ_FOREIGN(TPSVCINFO *p); void ACQ_UNMATCH(TPSVCINFO *p); +void ACQ_REPROC(TPSVCINFO *p); void ACQ_DUPCHK(TPSVCINFO *p); +void ACQ_LIMITCHK(TPSVCINFO *p); void ACQ_FEEADJ(TPSVCINFO *p); +void ACQ_STATUS(TPSVCINFO *p); void ACQ_MERCHSUM(TPSVCINFO *p); +void ACQ_ISSUERCLS(TPSVCINFO *p);void ACQ_WHT(TPSVCINFO *p); +void ACQ_TAXINV(TPSVCINFO *p); void ACQ_APPRLINK(TPSVCINFO *p); +void ACQ_DATECHG(TPSVCINFO *p); void ACQ_AMTFIX(TPSVCINFO *p); +void ACQ_RECLASS(TPSVCINFO *p); void ACQ_XFERLINK(TPSVCINFO *p); +void ACQ_APPRMAP(TPSVCINFO *p); void ACQ_SETTLELINK(TPSVCINFO *p); +void ACQ_HOLD(TPSVCINFO *p); void ACQ_RELEASE(TPSVCINFO *p); +void ACQ_VOID(TPSVCINFO *p); void ACQ_REFUND(TPSVCINFO *p); + +/* ----- advertise table (this is what "scales to 30" cleanly) ------------- */ +static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = { + {"ACQUIRE", ACQUIRE}, {"ACQ_DDC", ACQ_DDC}, + {"ACQ_EDI", ACQ_EDI}, {"ACQ_EDC", ACQ_EDC}, + {"ACQ_CANCEL", ACQ_CANCEL}, {"ACQ_CORRECT", ACQ_CORRECT}, + {"ACQ_PARTIAL", ACQ_PARTIAL}, {"ACQ_INSTALL", ACQ_INSTALL}, + {"ACQ_FOREIGN", ACQ_FOREIGN}, {"ACQ_UNMATCH", ACQ_UNMATCH}, + {"ACQ_REPROC", ACQ_REPROC}, {"ACQ_DUPCHK", ACQ_DUPCHK}, + {"ACQ_LIMITCHK", ACQ_LIMITCHK},{"ACQ_FEEADJ", ACQ_FEEADJ}, + {"ACQ_STATUS", ACQ_STATUS}, {"ACQ_MERCHSUM", ACQ_MERCHSUM}, + {"ACQ_ISSUERCLS", ACQ_ISSUERCLS},{"ACQ_WHT", ACQ_WHT}, + {"ACQ_TAXINV", ACQ_TAXINV}, {"ACQ_APPRLINK", ACQ_APPRLINK}, + {"ACQ_DATECHG", ACQ_DATECHG}, {"ACQ_AMTFIX", ACQ_AMTFIX}, + {"ACQ_RECLASS", ACQ_RECLASS}, {"ACQ_XFERLINK", ACQ_XFERLINK}, + {"ACQ_APPRMAP", ACQ_APPRMAP}, {"ACQ_SETTLELINK", ACQ_SETTLELINK}, + {"ACQ_HOLD", ACQ_HOLD}, {"ACQ_RELEASE", ACQ_RELEASE}, + {"ACQ_VOID", ACQ_VOID}, {"ACQ_REFUND", ACQ_REFUND}, + {NULL, NULL} +}; + +int tpsvrinit(int argc, char **argv) +{ + int i; + if (tpopen() < 0) { + userlog("ac_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("ac_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno)); + return -1; + } + } + userlog("ac_svr: %d ac services advertised, RM opened", i); + return 0; +} + +void tpsvrdone(void) +{ + tpclose(); + userlog("ac_svr: tpsvrdone"); +} + +/* ========================================================================= */ +/* 1. ACQUIRE - 매입 접수 + 승인, then the XA chain RECONCILE->SETTLE. */ +/* ========================================================================= */ +void ACQUIRE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16]; + long amount, bps = 25, fee, net, pid, rlen = 0; + + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] == 0) { userlog("ACQUIRE: missing T_MERCHANT"); FAIL(b); } + amount = getl(b, T_AMOUNT); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + + if (acdb_merchant_mdr(merch, &bps) < 0) bps = 25; /* keep 25bps default */ + fee = acq_fee(amount, bps); + net = amount - fee; + + pid = acdb_next_purchase_id(); + if (pid < 0) FAIL(b); + if (acdb_insert_purchase(pid, merch, amount, fee, net, "A", "POS", bizdate) < 0) FAIL(b); + if (acdb_insert_approval(pid, 1) < 0) FAIL(b); + + setl(b, T_PURCHASE_ID, pid); + setl(b, T_FEE, fee); + setl(b, T_NET, net); + Bchg(b, T_STATUS, 0, "A", 0L); + + userlog("ACQUIRE pid=%ld merch=%s amount=%ld fee=%ld net=%ld -> RECONCILE", + pid, merch, amount, fee, net); + + /* continue the SAME global tx into rc_svr (distinct branch) */ + if (tpcall("RECONCILE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) { + userlog("ACQUIRE: tpcall(RECONCILE) FAIL: %s", tpstrerror(tperrno)); + FAIL(b); + } + /* our branch resumes: settle-complete the purchase we own */ + if (acdb_update_status(pid, "S") < 0) FAIL(b); + Bchg(b, T_STATUS, 0, "S", 0L); + OK(b); +} + +/* 2. ACQ_DDC - Dynamic Currency Conversion: record fx leg for a purchase. */ +void ACQ_DDC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16], ccy[8]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_fx, h_krw, h_rate; + char h_ccy[8], h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + + h_pid = getl(b, T_PURCHASE_ID); + h_fx = getl(b, T_FX_AMT); + gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD"); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + h_rate = 1350; /* 13.50 KRW per unit (bps of 100) */ + h_krw = h_fx * h_rate / 100; + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + + EXEC SQL SELECT nextval('ac_fx_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_fx (fx_id, purchase_id, ccy, fx_amount, krw_amount, rate_bps, biz_date) + VALUES (:h_id, :h_pid, :h_ccy, :h_fx, :h_krw, :h_rate, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("ACQ_DDC INSERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + setl(b, T_AMOUNT, h_krw); + OK(b); +} + +/* 3. ACQ_EDI - EDI 청구 문서 접수. */ +void ACQ_EDI(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], docno[32], bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_amount; + char h_merch[64], h_doc[32], h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_DOCNO, docno, sizeof(docno)); if (docno[0] == 0) strcpy(docno, "EDI-AUTO"); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + h_amount = getl(b, T_AMOUNT); + strncpy(h_merch, merch, sizeof(h_merch)-1); h_merch[sizeof(h_merch)-1] = 0; + strncpy(h_doc, docno, sizeof(h_doc)-1); h_doc[sizeof(h_doc)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + + EXEC SQL SELECT nextval('ac_edi_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_edi (edi_id, merchant_id, doc_no, amount, biz_date) + VALUES (:h_id, :h_merch, :h_doc, :h_amount, :h_bizdate); + if (sqlca.sqlcode < 0) { userlog("ACQ_EDI INSERT FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + OK(b); +} + +/* 4. ACQ_EDC - EDC 단말 매입 접수 (채널=EDC, 체인 없음). */ +void ACQ_EDC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16]; + long amount, bps = 25, fee, net, pid; + + gets_(b, T_MERCHANT, merch, sizeof(merch)); + amount = getl(b, T_AMOUNT); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + if (acdb_merchant_mdr(merch, &bps) < 0) bps = 25; + fee = acq_fee(amount, bps); net = amount - fee; + pid = acdb_next_purchase_id(); + if (pid < 0) FAIL(b); + if (acdb_insert_purchase(pid, merch, amount, fee, net, "A", "EDC", bizdate) < 0) FAIL(b); + if (acdb_insert_approval(pid, 1) < 0) FAIL(b); + setl(b, T_PURCHASE_ID, pid); setl(b, T_FEE, fee); setl(b, T_NET, net); + OK(b); +} + +/* 5. ACQ_CANCEL - 전체 취소. */ +void ACQ_CANCEL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16], reason[128]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_amt; + char h_bizdate[16], h_reason[128]; + EXEC SQL END DECLARE SECTION; + + h_pid = getl(b, T_PURCHASE_ID); + h_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, "CUSTOMER"); + 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('ac_cancel_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_pid, 'FULL', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + if (acdb_update_status(h_pid, "C") < 0) FAIL(b); + setl(b, T_CANCEL_ID, h_id); + Bchg(b, T_STATUS, 0, "C", 0L); + OK(b); +} + +/* 6. ACQ_CORRECT - 금액 정정 + 수수료 재계산. */ +void ACQ_CORRECT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64]; + long pid, amount, bps = 25, fee, net; + pid = getl(b, T_PURCHASE_ID); amount = getl(b, T_AMOUNT); + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] && acdb_merchant_mdr(merch, &bps) < 0) bps = 25; + fee = acq_fee(amount, bps); net = amount - fee; + if (acdb_update_amount(pid, amount, fee, net) < 0) FAIL(b); + setl(b, T_FEE, fee); setl(b, T_NET, net); + OK(b); +} + +/* 7. ACQ_PARTIAL - 부분 취소. */ +void ACQ_PARTIAL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_amt; + char h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); 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 SELECT nextval('ac_cancel_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_pid, 'PARTIAL', :h_amt, 'PARTIAL', :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + setl(b, T_CANCEL_ID, h_id); + OK(b); +} + +/* 8. ACQ_INSTALL - 할부 개월 분할 등록. */ +void ACQ_INSTALL(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + long months, amount, i; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_seq, h_months, h_mamt; + char h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + + h_pid = getl(b, T_PURCHASE_ID); + months = getl(b, T_INSTALL_N); if (months < 1) months = 3; + amount = 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; + h_months = months; h_mamt = amount / months; + for (i = 1; i <= months; i++) { + h_seq = i; + EXEC SQL SELECT nextval('ac_install_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_install (install_id, purchase_id, seq_no, months, month_amount, biz_date) + VALUES (:h_id, :h_pid, :h_seq, :h_months, :h_mamt, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + } + setl(b, T_COUNT, months); + OK(b); +} + +/* 9. ACQ_FOREIGN - 해외 매입 (채널=FOREIGN) + fx leg. */ +void ACQ_FOREIGN(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16], ccy[8]; + long amount, bps = 30, fee, net, pid; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_fx, h_krw, h_rate; + char h_ccy[8], h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + + gets_(b, T_MERCHANT, merch, sizeof(merch)); + amount = getl(b, T_AMOUNT); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + gets_(b, T_CCY, ccy, sizeof(ccy)); if (ccy[0] == 0) strcpy(ccy, "USD"); + if (acdb_merchant_mdr(merch, &bps) < 0) bps = 30; + fee = acq_fee(amount, bps); net = amount - fee; + pid = acdb_next_purchase_id(); + if (pid < 0) FAIL(b); + if (acdb_insert_purchase(pid, merch, amount, fee, net, "A", "FOREIGN", bizdate) < 0) FAIL(b); + + h_pid = pid; h_fx = amount; h_rate = 1350; h_krw = amount; + strncpy(h_ccy, ccy, sizeof(h_ccy)-1); h_ccy[sizeof(h_ccy)-1] = 0; + strncpy(h_bizdate, bizdate, sizeof(h_bizdate)-1); h_bizdate[sizeof(h_bizdate)-1] = 0; + EXEC SQL SELECT nextval('ac_fx_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_fx (fx_id, purchase_id, ccy, fx_amount, krw_amount, rate_bps, biz_date) + VALUES (:h_id, :h_pid, :h_ccy, :h_fx, :h_krw, :h_rate, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + setl(b, T_PURCHASE_ID, pid); setl(b, T_FEE, fee); + OK(b); +} + +/* 10. ACQ_UNMATCH - 미매입(원장 없는 매입) 추출. */ +void ACQ_UNMATCH(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 ac_unmatch (unmatch_id, purchase_id, reason, biz_date) + SELECT nextval('ac_unmatch_seq'), p.purchase_id, 'NO_LEDGER', p.biz_date + FROM purchase p + WHERE p.biz_date = :h_bizdate + AND NOT EXISTS (SELECT 1 FROM ledger l WHERE l.purchase_id = p.purchase_id) + AND NOT EXISTS (SELECT 1 FROM ac_unmatch u WHERE u.purchase_id = p.purchase_id); + if (sqlca.sqlcode < 0) { userlog("ACQ_UNMATCH FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + h_cnt = sqlca.sqlerrd[2]; + setl(b, T_COUNT, h_cnt); + OK(b); +} + +/* 11. ACQ_REPROC - 재처리(상태 A로 되돌림). */ +void ACQ_REPROC(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + long pid = getl(b, T_PURCHASE_ID); + if (acdb_update_status(pid, "A") < 0) FAIL(b); + Bchg(b, T_STATUS, 0, "A", 0L); + OK(b); +} + +/* 12. ACQ_DUPCHK - 중복 매입 건수 조회. */ +void ACQ_DUPCHK(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)); + 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); + setl(b, T_COUNT, h_cnt); + OK(b); +} + +/* 13. ACQ_LIMITCHK - 가맹점 일한도 점검. */ +void ACQ_LIMITCHK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_sum, h_lim; + 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(amount),0) INTO :h_sum FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL SELECT daily_limit INTO :h_lim FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + setl(b, T_GROSS, h_sum); + setl(b, T_RC, h_sum > h_lim ? 1 : 0); + OK(b); +} + +/* 14. ACQ_FEEADJ - 수수료 조정 (dbio) + purchase 반영. */ +void ACQ_FEEADJ(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16], reason[128]; + long pid, delta, adj; + EXEC SQL BEGIN DECLARE SECTION; + long e_pid, e_delta; + EXEC SQL END DECLARE SECTION; + + pid = getl(b, T_PURCHASE_ID); + 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 = acdb_insert_fee_adj(pid, delta, reason, bizdate); + if (adj < 0) FAIL(b); + + e_pid = pid; e_delta = delta; + EXEC SQL UPDATE purchase SET fee = fee + :e_delta, net = net - :e_delta, updated_at = now() + WHERE purchase_id = :e_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + setl(b, T_ADJ, adj); + OK(b); +} + +/* 15. ACQ_STATUS - 매입 상태 조회 (dbio). */ +void ACQ_STATUS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + long pid = getl(b, T_PURCHASE_ID); + char st[4]; + if (acdb_get_status(pid, st) < 0) FAIL(b); + Bchg(b, T_STATUS, 0, st, 0L); + OK(b); +} + +/* 16. ACQ_MERCHSUM - 단일 가맹점 당일 집계 upsert (dbio). */ +void ACQ_MERCHSUM(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(amount),0), coalesce(sum(fee),0), coalesce(sum(net),0) + INTO :h_cnt, :h_gross, :h_fee, :h_net FROM purchase + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) FAIL(b); + if (acdb_upsert_merch_sum(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); +} + +/* 17. ACQ_ISSUERCLS - 발급사 분류 갱신. */ +void ACQ_ISSUERCLS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char issuer[32]; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid; + char h_issuer[32]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); + gets_(b, T_ISSUER, issuer, sizeof(issuer)); if (issuer[0] == 0) strcpy(issuer, "BC"); + strncpy(h_issuer, issuer, sizeof(h_issuer)-1); h_issuer[sizeof(h_issuer)-1] = 0; + EXEC SQL UPDATE purchase SET issuer = :h_issuer, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + OK(b); +} + +/* 18. ACQ_WHT - 원천징수 (수수료의 22%). */ +void ACQ_WHT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + long pid, tax, wht_id; + EXEC SQL BEGIN DECLARE SECTION; + long e_pid, e_fee; + EXEC SQL END DECLARE SECTION; + pid = getl(b, T_PURCHASE_ID); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + e_pid = pid; + EXEC SQL SELECT fee INTO :e_fee FROM purchase WHERE purchase_id = :e_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + tax = acq_wht_amt(e_fee, 2200); /* 22% */ + wht_id = acdb_insert_wht(pid, tax, bizdate); + if (wht_id < 0) FAIL(b); + setl(b, T_TAX, tax); + OK(b); +} + +/* 19. ACQ_TAXINV - 세금계산서 발행 (부가세 10%). */ +void ACQ_TAXINV(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16]; + long pid, supply, vat, inv; + pid = getl(b, T_PURCHASE_ID); + supply = getl(b, T_AMOUNT); + gets_(b, T_MERCHANT, merch, sizeof(merch)); + gets_(b, T_BIZDATE, bizdate, sizeof(bizdate)); + vat = acq_vat(supply); + inv = acdb_insert_tax_invoice(pid, merch, supply, vat, bizdate); + if (inv < 0) FAIL(b); + setl(b, T_TAX, vat); + OK(b); +} + +/* 20. ACQ_APPRLINK - 승인 연계 기록. */ +void ACQ_APPRLINK(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + long pid = getl(b, T_PURCHASE_ID); + if (acdb_insert_approval(pid, 1) < 0) FAIL(b); + OK(b); +} + +/* 21. ACQ_DATECHG - 영업일 변경. */ +void ACQ_DATECHG(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid; + char h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); + 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 purchase SET biz_date = :h_bizdate, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + OK(b); +} + +/* 22. ACQ_AMTFIX - 금액 확정 정정 (절대값). */ +void ACQ_AMTFIX(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64]; + long pid, amount, bps = 25, fee, net; + pid = getl(b, T_PURCHASE_ID); amount = getl(b, T_AMOUNT); + gets_(b, T_MERCHANT, merch, sizeof(merch)); + if (merch[0] && acdb_merchant_mdr(merch, &bps) < 0) bps = 25; + fee = acq_fee(amount, bps); net = amount - fee; + if (acdb_update_amount(pid, amount, fee, net) < 0) FAIL(b); + setl(b, T_FEE, fee); setl(b, T_NET, net); + OK(b); +} + +/* 23. ACQ_RECLASS - 업종/분류 재지정. */ +void ACQ_RECLASS(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char cat[32]; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid; + char h_cat[32]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); + gets_(b, T_CATEGORY, cat, sizeof(cat)); if (cat[0] == 0) strcpy(cat, "GEN"); + strncpy(h_cat, cat, sizeof(h_cat)-1); h_cat[sizeof(h_cat)-1] = 0; + EXEC SQL UPDATE purchase SET category = :h_cat, updated_at = now() WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) FAIL(b); + OK(b); +} + +/* 24. ACQ_XFERLINK - 이체 연계 원장 기록. */ +void ACQ_XFERLINK(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 (acdb_insert_ledger(pid, "XFER", amt, bizdate) < 0) FAIL(b); + OK(b); +} + +/* 25. ACQ_APPRMAP - 승인 매핑 건수 조회. */ +void ACQ_APPRMAP(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + 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); + setl(b, T_COUNT, h_cnt); + OK(b); +} + +/* 26. ACQ_SETTLELINK - 정산 연계 원장 기록. */ +void ACQ_SETTLELINK(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 (acdb_insert_ledger(pid, "SETTLELINK", net, bizdate) < 0) FAIL(b); + OK(b); +} + +/* 27. ACQ_HOLD - 매입 보류. */ +void ACQ_HOLD(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + long pid = getl(b, T_PURCHASE_ID); + if (acdb_update_status(pid, "H") < 0) FAIL(b); + Bchg(b, T_STATUS, 0, "H", 0L); + OK(b); +} + +/* 28. ACQ_RELEASE - 보류 해제. */ +void ACQ_RELEASE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + long pid = getl(b, T_PURCHASE_ID); + if (acdb_update_status(pid, "A") < 0) FAIL(b); + Bchg(b, T_STATUS, 0, "A", 0L); + OK(b); +} + +/* 29. ACQ_VOID - 매입 무효화 (당일 취소). */ +void ACQ_VOID(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_amt; + char h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); 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 SELECT nextval('ac_cancel_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_pid, 'VOID', :h_amt, 'VOID', :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + if (acdb_update_status(h_pid, "V") < 0) FAIL(b); + setl(b, T_CANCEL_ID, h_id); + Bchg(b, T_STATUS, 0, "V", 0L); + OK(b); +} + +/* 30. ACQ_REFUND - 환불 기록. */ +void ACQ_REFUND(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16], reason[128]; + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid, h_amt; + char h_bizdate[16], h_reason[128]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); h_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, "REFUND"); + 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('ac_cancel_seq') INTO :h_id; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO ac_cancel (cancel_id, purchase_id, cancel_type, amount, reason, biz_date) + VALUES (:h_id, :h_pid, 'REFUND', :h_amt, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + setl(b, T_CANCEL_ID, h_id); + OK(b); +} + +/* vim: set ts=4 sw=4 et smartindent: */ diff --git a/app/src/ac/batch/ac_merchsum_batch.pgc b/app/src/ac/batch/ac_merchsum_batch.pgc new file mode 100644 index 0000000..5fe8e4a --- /dev/null +++ b/app/src/ac/batch/ac_merchsum_batch.pgc @@ -0,0 +1,56 @@ +/* + * ac_merchsum_batch.pgc - ac 가맹점 일집계 배치 (커서 + XA). + * + * ATMI client: tpinit/tpopen open the ECPG XA RM, tpbegin starts ONE global + * transaction, an EXEC SQL cursor aggregates purchase rows per merchant for a + * biz_date and upserts merchant_summary (via the linked dbio lib), then tpcommit + * drives XA 2PC. Usage: ac_merchsum_batch [YYYY-MM-DD] + */ +#include +#include +#include +#include +#include "ac_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_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 msc CURSOR FOR + SELECT merchant_id, count(*), coalesce(sum(amount),0), + coalesce(sum(fee),0), coalesce(sum(net),0) + FROM purchase WHERE biz_date = :h_bizdate + GROUP BY merchant_id ORDER BY merchant_id; + 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_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 msc; tpabort(0); return 1; } + if (acdb_upsert_merch_sum(h_merch, bizdate, h_gross, h_fee, h_net, h_cnt) < 0) { + 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(">>> ac_merchsum_batch COMMIT: bizdate=%s merchants=%ld\n", bizdate, rows); + + tpclose(); tpterm(); + return 0; +} diff --git a/app/src/ac/batch/ac_wht_batch.pgc b/app/src/ac/batch/ac_wht_batch.pgc new file mode 100644 index 0000000..8e4687a --- /dev/null +++ b/app/src/ac/batch/ac_wht_batch.pgc @@ -0,0 +1,55 @@ +/* + * ac_wht_batch.pgc - ac 원천징수 배치 (커서 + XA). + * + * Cursors over settled purchases of a biz_date that have no withholding row yet + * and inserts a 22% withholding record (via dbio) for each, all under ONE global + * XA transaction. Usage: ac_wht_batch [YYYY-MM-DD] + */ +#include +#include +#include +#include +#include "ac_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]; + long h_pid, h_fee; + 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 whc CURSOR FOR + SELECT p.purchase_id, p.fee FROM purchase p + WHERE p.biz_date = :h_bizdate + AND NOT EXISTS (SELECT 1 FROM ac_wht w WHERE w.purchase_id = p.purchase_id) + ORDER BY p.purchase_id; + EXEC SQL OPEN whc; + if (sqlca.sqlcode < 0) { fprintf(stderr, "OPEN FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); tpabort(0); return 1; } + + for (;;) { + EXEC SQL FETCH whc INTO :h_pid, :h_fee; + if (sqlca.sqlcode == 100) break; + if (sqlca.sqlcode < 0) { fprintf(stderr, "FETCH FAIL [%d] %s\n", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); EXEC SQL CLOSE whc; tpabort(0); return 1; } + if (acdb_insert_wht(h_pid, acq_wht_amt(h_fee, 2200), bizdate) < 0) { + EXEC SQL CLOSE whc; tpabort(0); return 1; + } + rows++; + } + EXEC SQL CLOSE whc; + + if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; } + printf(">>> ac_wht_batch COMMIT: bizdate=%s wht_rows=%ld\n", bizdate, rows); + + tpclose(); tpterm(); + return 0; +} diff --git a/app/src/ac/dbio/ac_dbio.h b/app/src/ac/dbio/ac_dbio.h new file mode 100644 index 0000000..5d3cca2 --- /dev/null +++ b/app/src/ac/dbio/ac_dbio.h @@ -0,0 +1,35 @@ +/* + * ac_dbio.h - ac 모듈 DB 접근 계층 (libacdbio.a). + * These are ECPG (EXEC SQL) functions compiled into a static library and linked + * into ac_svr and the ac 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 AC_DBIO_H +#define AC_DBIO_H + +/* purchase */ +long acdb_next_purchase_id(void); +int acdb_insert_purchase(long pid, const char *merch, long amount, long fee, + long net, const char *status, const char *channel, + const char *bizdate); +int acdb_update_status(long pid, const char *status); +int acdb_get_status(long pid, char *out /* >= 2 bytes */); +int acdb_update_amount(long pid, long amount, long fee, long net); +int acdb_insert_approval(long pid, int approved); + +/* fee / tax */ +long acdb_insert_fee_adj(long pid, long delta, const char *reason, const char *bizdate); +long acdb_insert_wht(long pid, long tax, const char *bizdate); +long acdb_insert_tax_invoice(long pid, const char *merch, long supply, long vat, + const char *bizdate); + +/* merchant */ +int acdb_merchant_mdr(const char *merch, long *bps_out); +int acdb_upsert_merch_sum(const char *merch, const char *bizdate, long gross, + long fee, long net, long cnt); + +/* misc ledger */ +long acdb_insert_ledger(long pid, const char *type, long amount, const char *bizdate); + +#endif /* AC_DBIO_H */ diff --git a/app/src/ac/dbio/ac_fee_dbio.pgc b/app/src/ac/dbio/ac_fee_dbio.pgc new file mode 100644 index 0000000..551ecac --- /dev/null +++ b/app/src/ac/dbio/ac_fee_dbio.pgc @@ -0,0 +1,72 @@ +/* + * ac_fee_dbio.pgc - fee-adjust / withholding / tax-invoice DB access (libacdbio.a). + */ +#include +#include +#include "ac_dbio.h" + +long acdb_insert_fee_adj(long pid, long delta, const char *reason, const char *bizdate) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_id, 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('ac_feeadj_seq') INTO :h_id; + if (sqlca.sqlcode < 0) return -1; + + EXEC SQL INSERT INTO ac_fee_adj (adj_id, purchase_id, delta, reason, biz_date) + VALUES (:h_id, :h_pid, :h_delta, :h_reason, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("acdb_insert_fee_adj FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return h_id; +} + +long acdb_insert_wht(long pid, long tax, const char *bizdate) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid = pid, h_tax = tax; + 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 nextval('ac_wht_seq') INTO :h_id; + if (sqlca.sqlcode < 0) return -1; + + EXEC SQL INSERT INTO ac_wht (wht_id, purchase_id, tax_amount, biz_date) + VALUES (:h_id, :h_pid, :h_tax, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("acdb_insert_wht FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return h_id; +} + +long acdb_insert_tax_invoice(long pid, const char *merch, long supply, long vat, + const char *bizdate) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_id, h_pid = pid, h_supply = supply, h_vat = vat; + 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 SELECT nextval('ac_taxinv_seq') INTO :h_id; + if (sqlca.sqlcode < 0) return -1; + + EXEC SQL INSERT INTO ac_tax_invoice (inv_id, purchase_id, merchant_id, supply, vat, biz_date) + VALUES (:h_id, :h_pid, :h_merch, :h_supply, :h_vat, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("acdb_insert_tax_invoice FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return h_id; +} diff --git a/app/src/ac/dbio/ac_merch_dbio.pgc b/app/src/ac/dbio/ac_merch_dbio.pgc new file mode 100644 index 0000000..dcdbb1d --- /dev/null +++ b/app/src/ac/dbio/ac_merch_dbio.pgc @@ -0,0 +1,51 @@ +/* + * ac_merch_dbio.pgc - merchant lookup + daily summary upsert (libacdbio.a). + */ +#include +#include +#include "ac_dbio.h" + +int acdb_merchant_mdr(const char *merch, long *bps_out) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_bps; + 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 mdr_bps INTO :h_bps FROM merchant WHERE merchant_id = :h_merch; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { + userlog("acdb_merchant_mdr FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + *bps_out = h_bps; + return 0; +} + +int acdb_upsert_merch_sum(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 merchant_summary + (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 = merchant_summary.txn_count + EXCLUDED.txn_count, + gross_amount = merchant_summary.gross_amount + EXCLUDED.gross_amount, + fee_amount = merchant_summary.fee_amount + EXCLUDED.fee_amount, + net_amount = merchant_summary.net_amount + EXCLUDED.net_amount, + updated_at = now(); + if (sqlca.sqlcode < 0) { + userlog("acdb_upsert_merch_sum FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return 0; +} diff --git a/app/src/ac/dbio/ac_purchase_dbio.pgc b/app/src/ac/dbio/ac_purchase_dbio.pgc new file mode 100644 index 0000000..97e9fb3 --- /dev/null +++ b/app/src/ac/dbio/ac_purchase_dbio.pgc @@ -0,0 +1,131 @@ +/* + * ac_purchase_dbio.pgc - purchase/approval/ledger DB access (part of libacdbio.a). + * ECPG functions; run on the caller's XA branch. No EXEC SQL CONNECT. + */ +#include +#include +#include "ac_dbio.h" + +long acdb_next_purchase_id(void) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_id; + EXEC SQL END DECLARE SECTION; + + EXEC SQL SELECT nextval('purchase_seq') INTO :h_id; + if (sqlca.sqlcode < 0) { + userlog("acdb_next_purchase_id FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return h_id; +} + +int acdb_insert_purchase(long pid, const char *merch, long amount, long fee, + long net, const char *status, const char *channel, + const char *bizdate) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_pid = pid, h_amount = amount, h_fee = fee, h_net = net; + char h_merch[64], 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_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 purchase + (purchase_id, merchant_id, amount, fee, net, status, biz_date, channel) + VALUES (:h_pid, :h_merch, :h_amount, :h_fee, :h_net, :h_status, :h_bizdate, :h_channel); + if (sqlca.sqlcode < 0) { + userlog("acdb_insert_purchase FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return 0; +} + +int acdb_insert_approval(long pid, int approved) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_pid = pid; + int h_ok = approved; + EXEC SQL END DECLARE SECTION; + + EXEC SQL INSERT INTO approval (approval_id, purchase_id, approved) + VALUES (nextval('approval_seq'), :h_pid, :h_ok); + if (sqlca.sqlcode < 0) { + userlog("acdb_insert_approval FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return 0; +} + +int acdb_update_status(long pid, const char *status) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_pid = pid; + 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 purchase SET status = :h_status, updated_at = now() + WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { + userlog("acdb_update_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return 0; +} + +int acdb_get_status(long pid, char *out) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_pid = pid; + char h_status[8]; + EXEC SQL END DECLARE SECTION; + + EXEC SQL SELECT status INTO :h_status FROM purchase WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { + userlog("acdb_get_status FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + out[0] = h_status[0]; + out[1] = 0; + return 0; +} + +int acdb_update_amount(long pid, long amount, long fee, long net) +{ + EXEC SQL BEGIN DECLARE SECTION; + long h_pid = pid, h_amount = amount, h_fee = fee, h_net = net; + EXEC SQL END DECLARE SECTION; + + EXEC SQL UPDATE purchase + SET amount = :h_amount, fee = :h_fee, net = :h_net, updated_at = now() + WHERE purchase_id = :h_pid; + if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { + userlog("acdb_update_amount FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return 0; +} + +long acdb_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("acdb_insert_ledger FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + return -1; + } + return 0; +} diff --git a/app/src/acquire.pgc b/app/src/acquire.pgc deleted file mode 100644 index 19e7b11..0000000 --- a/app/src/acquire.pgc +++ /dev/null @@ -1,142 +0,0 @@ -/* - * ACQUIRE (매입 접수) - ECPG XA service. - * - * Inserts a `purchase` row (fee = amount * 25/10000 MDR, net = amount - fee), - * then within the SAME global transaction tpcall()s RECONCILE. - * - * XA connection is opened by the ECPG XA switch (libndrxxaecpg.so) via tpopen(); - * there is deliberately NO `EXEC SQL CONNECT` (matches ref/atmisv67.pgc). - */ -#include -#include -#include -#include -#include -#include -#include -#include "acq.fd.h" - -void ACQUIRE(TPSVCINFO *p); - -int tpsvrinit(int argc, char **argv) -{ - if (tpopen() < 0) { - userlog("acquire: tpopen FAIL: %s", tpstrerror(tperrno)); - return -1; - } - if (tpadvertise("ACQUIRE", ACQUIRE) < 0) { - userlog("acquire: tpadvertise(ACQUIRE) FAIL: %s", tpstrerror(tperrno)); - return -1; - } - userlog("acquire: ACQUIRE advertised, RM opened"); - return 0; -} - -void tpsvrdone(void) -{ - tpclose(); - userlog("acquire: tpsvrdone"); -} - -void ACQUIRE(TPSVCINFO *p) -{ - UBFH *b = (UBFH *)p->data; - long amount = 0, fee = 0, net = 0; - char merch[64] = ""; - char bizdate[16] = ""; - BFLDLEN len; - long rlen = 0; - - EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - long h_amount; - long h_fee; - long h_net; - char h_merch[64]; - char h_bizdate[16]; - EXEC SQL END DECLARE SECTION; - - len = sizeof(merch); - if (Bget(b, T_MERCHANT, 0, merch, &len) < 0) { - userlog("ACQUIRE: missing T_MERCHANT: %s", Bstrerror(Berror)); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - Bget(b, T_AMOUNT, 0, (char *)&amount, 0L); - len = sizeof(bizdate); - Bget(b, T_BIZDATE, 0, bizdate, &len); - - fee = amount * 25 / 10000; /* 0.25% MDR */ - net = amount - fee; - - /* obtain a purchase id (sequence is non-transactional by design) */ - EXEC SQL SELECT nextval('purchase_seq') INTO :h_pid; - if (sqlca.sqlcode < 0) { - userlog("ACQUIRE: nextval FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - h_amount = amount; - h_fee = fee; - h_net = net; - 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 purchase - (purchase_id, merchant_id, amount, fee, net, status, biz_date) - VALUES (:h_pid, :h_merch, :h_amount, :h_fee, :h_net, 'A', :h_bizdate); - if (sqlca.sqlcode < 0) { - userlog("ACQUIRE: INSERT purchase FAIL [%d] %s", - sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - /* record the approval as well (all under the same global tx) */ - EXEC SQL INSERT INTO approval (approval_id, purchase_id, approved) - VALUES (nextval('approval_seq'), :h_pid, true); - if (sqlca.sqlcode < 0) { - userlog("ACQUIRE: INSERT approval FAIL [%d] %s", - sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - Bchg(b, T_PURCHASE_ID, 0, (char *)&h_pid, 0L); - Bchg(b, T_FEE, 0, (char *)&fee, 0L); - Bchg(b, T_NET, 0, (char *)&net, 0L); - Bchg(b, T_STATUS, 0, "A", 0L); - - userlog("ACQUIRE pid=%ld merch=%s amount=%ld fee=%ld net=%ld -> RECONCILE", - h_pid, merch, amount, fee, net); - - /* Continue the chain under the SAME global transaction. RECONCILE (and, in - * turn, SETTLE) run as separate XA branches (separate DB connections) and - * therefore CANNOT see this not-yet-committed purchase row - so they write - * only their OWN rows (ledger/settlement). This owner branch performs the - * purchase status transition itself once the chain returns, because its - * connection is resumed on the same branch and can see its own INSERT. */ - if (tpcall("RECONCILE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) { - userlog("ACQUIRE: tpcall(RECONCILE) FAIL: %s", tpstrerror(tperrno)); - tpreturn(TPFAIL, 0, (char *)b, 0L, 0L); - return; - } - - /* branch A resumed here: settle-complete the purchase we own */ - EXEC SQL UPDATE purchase - SET status = 'S', updated_at = now() - WHERE purchase_id = :h_pid; - if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { - userlog("ACQUIRE: UPDATE purchase status FAIL [%d] %s", - sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, (char *)b, 0L, 0L); - return; - } - - Bchg(b, T_STATUS, 0, "S", 0L); - - tpreturn(TPSUCCESS, 0, (char *)b, 0L, 0L); -} - -/* vim: set ts=4 sw=4 et smartindent: */ diff --git a/app/src/clients/ac_ops.c b/app/src/clients/ac_ops.c new file mode 100644 index 0000000..996e956 --- /dev/null +++ b/app/src/clients/ac_ops.c @@ -0,0 +1,65 @@ +/* + * ac_ops - exercises several ac_svr ONLINE services (not the chain) under ONE + * global XA transaction, to prove the module server's advertised services work + * and commit. Usage: ac_ops [merchant] [amount] [bizdate] + */ +#include +#include +#include +#include +#include +#include "acq.fd.h" + +static long callsvc(const char *svc, UBFH **b) +{ + long rlen = 0; + if (tpcall((char *)svc, (char *)*b, 0L, (char **)b, &rlen, 0L) < 0) { + fprintf(stderr, "tpcall(%s) FAIL: %s\n", svc, tpstrerror(tperrno)); + return -1; + } + return 0; +} + +int main(int argc, char **argv) +{ + long pid = (argc > 1) ? atol(argv[1]) : 1; + const char *merch = (argc > 2) ? argv[2] : "M0001"; + long amount = (argc > 3) ? atol(argv[3]) : 1000000; + const char *bizdate = (argc > 4) ? argv[4] : "2026-07-19"; + UBFH *b; + long cnt = 0, adj = 0, months = 0, delta = 100; + char st[16] = ""; + BFLDLEN l; + + 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; } + b = (UBFH *)tpalloc("UBF", NULL, 4096); + if (!b) { fprintf(stderr, "tpalloc FAIL\n"); return 1; } + + Bchg(b, T_PURCHASE_ID, 0, (char *)&pid, 0L); + Bchg(b, T_MERCHANT, 0, (char *)merch, 0L); + Bchg(b, T_AMOUNT, 0, (char *)&amount, 0L); + Bchg(b, T_BIZDATE, 0, (char *)bizdate, 0L); + Bchg(b, T_DELTA, 0, (char *)&delta, 0L); + Bchg(b, T_INSTALL_N, 0, (char *)(long[]){3}, 0L); + Bchg(b, T_REASON, 0, "OPS_TEST", 0L); + + if (tpbegin(60, 0) < 0) { fprintf(stderr, "tpbegin FAIL: %s\n", tpstrerror(tperrno)); return 1; } + + if (callsvc("ACQ_DUPCHK", &b) < 0) { tpabort(0); return 1; } + Bget(b, T_COUNT, 0, (char *)&cnt, 0L); + if (callsvc("ACQ_FEEADJ", &b) < 0) { tpabort(0); return 1; } + Bget(b, T_ADJ, 0, (char *)&adj, 0L); + if (callsvc("ACQ_INSTALL", &b) < 0) { tpabort(0); return 1; } + Bget(b, T_COUNT, 0, (char *)&months, 0L); + if (callsvc("ACQ_STATUS", &b) < 0) { tpabort(0); return 1; } + l = sizeof(st); Bget(b, T_STATUS, 0, st, &l); + + if (tpcommit(0) < 0) { fprintf(stderr, "tpcommit FAIL: %s\n", tpstrerror(tperrno)); tpabort(0); return 1; } + + printf(">>> ac_ops COMMIT: pid=%ld dupchk_count=%ld fee_adj_id=%ld install_rows=%ld status=%s\n", + pid, cnt, adj, months, st); + + tpfree((char *)b); tpclose(); tpterm(); + return 0; +} diff --git a/app/src/acqdrv.c b/app/src/clients/acqdrv.c similarity index 100% rename from app/src/acqdrv.c rename to app/src/clients/acqdrv.c diff --git a/app/src/common/acq_bizday.c b/app/src/common/acq_bizday.c new file mode 100644 index 0000000..6bd8790 --- /dev/null +++ b/app/src/common/acq_bizday.c @@ -0,0 +1,36 @@ +/* acq_bizday.c - business-day arithmetic (leaf util). */ +#include +#include "acq_common.h" + +static int is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); } + +static int mdays(int y, int m) +{ + static const int d[] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (m == 2 && is_leap(y)) return 29; + return d[m - 1]; +} + +/* Sakamoto's algorithm: day of week 0=Sun..6=Sat. */ +static int 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; +} + +int acq_next_bizday(const char *in, char *out) +{ + int y, m, d; + if (in == 0 || out == 0) return -1; + if (sscanf(in, "%d-%d-%d", &y, &m, &d) != 3) return -1; + if (m < 1 || m > 12 || d < 1 || d > 31) return -1; + + do { + d++; + if (d > mdays(y, m)) { d = 1; m++; if (m > 12) { m = 1; y++; } } + } while (dow(y, m, d) == 0 || dow(y, m, d) == 6); /* skip Sun/Sat */ + + sprintf(out, "%04d-%02d-%02d", y, m, d); + return 0; +} diff --git a/app/src/common/acq_common.h b/app/src/common/acq_common.h new file mode 100644 index 0000000..dfeb555 --- /dev/null +++ b/app/src/common/acq_common.h @@ -0,0 +1,27 @@ +/* + * acq_common.h - leaf utility library (libacqcommon.a). + * Pure C, no ATMI / no EXEC SQL. Linked into every module server and batch. + */ +#ifndef ACQ_COMMON_H +#define ACQ_COMMON_H + +/* fee = amount * bps / 10000 (basis points). */ +long acq_fee(long amount, long bps); + +/* Korean VAT 10% on a supply value (부가세). */ +long acq_vat(long supply); + +/* withholding tax on a fee, in basis points (원천징수). */ +long acq_wht_amt(long fee, long bps); + +/* Luhn (mod-10) check of a numeric PAN string. 1=valid, 0=invalid. */ +int acq_luhn_ok(const char *pan); + +/* Next business day for "YYYY-MM-DD": +1 day, skipping Sat/Sun. + * Writes "YYYY-MM-DD" into out (>= 11 bytes). Returns 0 ok, -1 on parse error. */ +int acq_next_bizday(const char *in, char *out); + +/* process-local monotonic sequence (demo of a common leaf util). */ +long acq_seq_next(void); + +#endif /* ACQ_COMMON_H */ diff --git a/app/src/common/acq_fee.c b/app/src/common/acq_fee.c new file mode 100644 index 0000000..6c1620d --- /dev/null +++ b/app/src/common/acq_fee.c @@ -0,0 +1,20 @@ +/* acq_fee.c - fee / tax math (leaf util). */ +#include "acq_common.h" + +long acq_fee(long amount, long bps) +{ + if (amount < 0 || bps < 0) return 0; + return amount * bps / 10000; +} + +long acq_vat(long supply) +{ + if (supply < 0) return 0; + return supply / 10; /* 10% VAT */ +} + +long acq_wht_amt(long fee, long bps) +{ + if (fee < 0 || bps < 0) return 0; + return fee * bps / 10000; +} diff --git a/app/src/common/acq_luhn.c b/app/src/common/acq_luhn.c new file mode 100644 index 0000000..df6950b --- /dev/null +++ b/app/src/common/acq_luhn.c @@ -0,0 +1,19 @@ +/* acq_luhn.c - Luhn (mod-10) card number check (leaf util). */ +#include "acq_common.h" + +int acq_luhn_ok(const char *pan) +{ + int sum = 0, alt = 0, i, n; + if (pan == 0) return 0; + for (n = 0; pan[n]; n++) ; /* strlen */ + if (n == 0) return 0; + for (i = n - 1; i >= 0; i--) { + char c = pan[i]; + if (c < '0' || c > '9') return 0; + int d = c - '0'; + if (alt) { d *= 2; if (d > 9) d -= 9; } + sum += d; + alt = !alt; + } + return (sum % 10) == 0; +} diff --git a/app/src/common/acq_seq.c b/app/src/common/acq_seq.c new file mode 100644 index 0000000..6eba10e --- /dev/null +++ b/app/src/common/acq_seq.c @@ -0,0 +1,9 @@ +/* acq_seq.c - process-local monotonic sequence (leaf util). */ +#include "acq_common.h" + +static long g_seq = 0; + +long acq_seq_next(void) +{ + return ++g_seq; +} diff --git a/app/src/rc/rc_svr.pgc b/app/src/rc/rc_svr.pgc new file mode 100644 index 0000000..8411d71 --- /dev/null +++ b/app/src/rc/rc_svr.pgc @@ -0,0 +1,137 @@ +/* + * 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include "acq.fd.h" + +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); } + +#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); + +static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = { + {"RECONCILE", RECONCILE}, {"RC_REMATCH", RC_REMATCH}, + {"RC_UNMATCH", RC_UNMATCH}, {"RC_HOLD", RC_HOLD}, + {NULL, NULL} +}; + +int tpsvrinit(int argc, char **argv) +{ + int i; + if (tpopen() < 0) { userlog("rc_svr: tpopen FAIL: %s", tpstrerror(tperrno)); return -1; } + for (i = 0; SVCS[i].name; i++) + if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) { + userlog("rc_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno)); + return -1; + } + userlog("rc_svr: %d rc services advertised, RM opened", i); + return 0; +} + +void tpsvrdone(void) { tpclose(); userlog("rc_svr: tpsvrdone"); } + +void RECONCILE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + long rlen = 0; + 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; + + /* own-branch write: recon ledger evidence only */ + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'RECON', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) { + userlog("RECONCILE INSERT ledger FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); + FAIL(b); + } + Bchg(b, T_STATUS, 0, "M", 0L); + userlog("RECONCILE pid=%ld matched -> SETTLE", h_pid); + + if (tpcall("SETTLE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) { + userlog("RECONCILE: tpcall(SETTLE) FAIL: %s", tpstrerror(tperrno)); + FAIL(b); + } + OK(b); +} + +/* RC_REMATCH - 재대사 원장 기록. */ +void RC_REMATCH(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, 'REMATCH', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + OK(b); +} + +/* RC_UNMATCH - 미대사 건수 조회. */ +void RC_UNMATCH(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 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) FAIL(b); + { long v = h_cnt; Bchg(b, T_COUNT, 0, (char *)&v, 0L); } + OK(b); +} + +/* RC_HOLD - 대사 보류 원장 기록. */ +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); +} + +/* vim: set ts=4 sw=4 et smartindent: */ diff --git a/app/src/reconcile.pgc b/app/src/reconcile.pgc deleted file mode 100644 index d04b6f9..0000000 --- a/app/src/reconcile.pgc +++ /dev/null @@ -1,88 +0,0 @@ -/* - * RECONCILE (대사) - ECPG XA service. - * - * Marks the purchase matched (status='M'), writes a ledger recon entry, then - * tpcall()s SETTLE - all inside the caller's global transaction. - */ -#include -#include -#include -#include -#include -#include -#include -#include "acq.fd.h" - -void RECONCILE(TPSVCINFO *p); - -int tpsvrinit(int argc, char **argv) -{ - if (tpopen() < 0) { - userlog("reconcile: tpopen FAIL: %s", tpstrerror(tperrno)); - return -1; - } - if (tpadvertise("RECONCILE", RECONCILE) < 0) { - userlog("reconcile: tpadvertise(RECONCILE) FAIL: %s", tpstrerror(tperrno)); - return -1; - } - userlog("reconcile: RECONCILE advertised, RM opened"); - return 0; -} - -void tpsvrdone(void) -{ - tpclose(); - userlog("reconcile: tpsvrdone"); -} - -void RECONCILE(TPSVCINFO *p) -{ - UBFH *b = (UBFH *)p->data; - long net = 0; - char bizdate[16] = ""; - BFLDLEN len; - long rlen = 0; - - EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - long h_net; - char h_bizdate[16]; - EXEC SQL END DECLARE SECTION; - - if (Bget(b, T_PURCHASE_ID, 0, (char *)&h_pid, 0L) < 0) { - userlog("RECONCILE: missing T_PURCHASE_ID: %s", Bstrerror(Berror)); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - Bget(b, T_NET, 0, (char *)&net, 0L); - len = sizeof(bizdate); - Bget(b, T_BIZDATE, 0, bizdate, &len); - h_net = net; - strncpy(h_bizdate, bizdate, sizeof(h_bizdate) - 1); h_bizdate[sizeof(h_bizdate) - 1] = 0; - - /* This is a distinct XA branch (own DB connection); it writes only its own - * recon evidence row and does NOT touch the purchase row (which belongs to - * the ACQUIRE branch and is not yet committed / not visible here). */ - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'RECON', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) { - userlog("RECONCILE: INSERT ledger FAIL [%d] %s", - sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - Bchg(b, T_STATUS, 0, "M", 0L); - - userlog("RECONCILE pid=%ld matched -> SETTLE", h_pid); - - if (tpcall("SETTLE", (char *)b, 0L, (char **)&b, &rlen, 0L) < 0) { - userlog("RECONCILE: tpcall(SETTLE) FAIL: %s", tpstrerror(tperrno)); - tpreturn(TPFAIL, 0, (char *)b, 0L, 0L); - return; - } - - tpreturn(TPSUCCESS, 0, (char *)b, 0L, 0L); -} - -/* vim: set ts=4 sw=4 et smartindent: */ diff --git a/app/src/settle.pgc b/app/src/settle.pgc deleted file mode 100644 index 99c4b1f..0000000 --- a/app/src/settle.pgc +++ /dev/null @@ -1,107 +0,0 @@ -/* - * SETTLE (정산) - ECPG XA service. - * - * Inserts a `settlement` (netting) row, writes a ledger settle entry, and marks - * the purchase settled (status='S'). Terminal service of the chain; still runs - * inside the caller's global transaction. - */ -#include -#include -#include -#include -#include -#include -#include -#include "acq.fd.h" - -void SETTLE(TPSVCINFO *p); - -int tpsvrinit(int argc, char **argv) -{ - if (tpopen() < 0) { - userlog("settle: tpopen FAIL: %s", tpstrerror(tperrno)); - return -1; - } - if (tpadvertise("SETTLE", SETTLE) < 0) { - userlog("settle: tpadvertise(SETTLE) FAIL: %s", tpstrerror(tperrno)); - return -1; - } - userlog("settle: SETTLE advertised, RM opened"); - return 0; -} - -void tpsvrdone(void) -{ - tpclose(); - userlog("settle: tpsvrdone"); -} - -void SETTLE(TPSVCINFO *p) -{ - UBFH *b = (UBFH *)p->data; - long net = 0; - char merch[64] = ""; - char bizdate[16] = ""; - BFLDLEN len; - - EXEC SQL BEGIN DECLARE SECTION; - long h_pid; - long h_sid; - long h_net; - char h_merch[64]; - char h_bizdate[16]; - EXEC SQL END DECLARE SECTION; - - if (Bget(b, T_PURCHASE_ID, 0, (char *)&h_pid, 0L) < 0) { - userlog("SETTLE: missing T_PURCHASE_ID: %s", Bstrerror(Berror)); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - Bget(b, T_NET, 0, (char *)&net, 0L); - len = sizeof(merch); - Bget(b, T_MERCHANT, 0, merch, &len); - len = sizeof(bizdate); - Bget(b, T_BIZDATE, 0, bizdate, &len); - - h_net = net; - 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('settlement_seq') INTO :h_sid; - if (sqlca.sqlcode < 0) { - userlog("SETTLE: nextval FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - EXEC SQL INSERT INTO settlement - (settlement_id, purchase_id, merchant_id, net, biz_date, status) - VALUES (:h_sid, :h_pid, :h_merch, :h_net, :h_bizdate, 'SETTLED'); - if (sqlca.sqlcode < 0) { - userlog("SETTLE: INSERT settlement FAIL [%d] %s", - sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - /* SETTLE is its own XA branch: it writes the settlement + a settle ledger - * entry (its own rows). The purchase status transition is performed by the - * ACQUIRE branch that owns that row. */ - EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) - VALUES (nextval('ledger_seq'), :h_pid, 'SETTLE', :h_net, :h_bizdate); - if (sqlca.sqlcode < 0) { - userlog("SETTLE: INSERT ledger FAIL [%d] %s", - sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); - tpreturn(TPFAIL, 0, p->data, 0L, 0L); - return; - } - - Bchg(b, T_SETTLE_ID, 0, (char *)&h_sid, 0L); - Bchg(b, T_STATUS, 0, "S", 0L); - - userlog("SETTLE pid=%ld settlement_id=%ld net=%ld status=S", h_pid, h_sid, net); - - tpreturn(TPSUCCESS, 0, (char *)b, 0L, 0L); -} - -/* vim: set ts=4 sw=4 et smartindent: */ diff --git a/app/src/st/st_svr.pgc b/app/src/st/st_svr.pgc new file mode 100644 index 0000000..3f0da2c --- /dev/null +++ b/app/src/st/st_svr.pgc @@ -0,0 +1,139 @@ +/* + * 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include "acq.fd.h" + +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 SETTLE(TPSVCINFO *p); +void ST_MDR(TPSVCINFO *p); +void ST_NETTING(TPSVCINFO *p); +void ST_VAT(TPSVCINFO *p); + +static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = { + {"SETTLE", SETTLE}, {"ST_MDR", ST_MDR}, + {"ST_NETTING", ST_NETTING}, {"ST_VAT", ST_VAT}, + {NULL, NULL} +}; + +int tpsvrinit(int argc, char **argv) +{ + int i; + if (tpopen() < 0) { userlog("st_svr: tpopen FAIL: %s", tpstrerror(tperrno)); return -1; } + for (i = 0; SVCS[i].name; i++) + if (tpadvertise((char *)SVCS[i].name, SVCS[i].fn) < 0) { + userlog("st_svr: tpadvertise(%s) FAIL: %s", SVCS[i].name, tpstrerror(tperrno)); + return -1; + } + userlog("st_svr: %d st services advertised, RM opened", i); + return 0; +} + +void tpsvrdone(void) { tpclose(); userlog("st_svr: tpsvrdone"); } + +void SETTLE(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_sid, h_net; + char h_merch[64], h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + + h_pid = getl(b, T_PURCHASE_ID); + h_net = getl(b, T_NET); + 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 nextval('settlement_seq') INTO :h_sid; + if (sqlca.sqlcode < 0) FAIL(b); + EXEC SQL INSERT INTO settlement (settlement_id, purchase_id, merchant_id, net, biz_date, status) + VALUES (:h_sid, :h_pid, :h_merch, :h_net, :h_bizdate, 'SETTLED'); + if (sqlca.sqlcode < 0) { userlog("SETTLE INSERT settlement FAIL [%d] %s", sqlca.sqlcode, sqlca.sqlerrm.sqlerrmc); FAIL(b); } + EXEC SQL INSERT INTO ledger (ledger_id, purchase_id, entry_type, amount, biz_date) + VALUES (nextval('ledger_seq'), :h_pid, 'SETTLE', :h_net, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + + setl(b, T_SETTLE_ID, h_sid); + Bchg(b, T_STATUS, 0, "S", 0L); + userlog("SETTLE pid=%ld settlement_id=%ld net=%ld", h_pid, h_sid, h_net); + OK(b); +} + +/* ST_MDR - MDR 수수료 원장 기록. */ +void ST_MDR(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_fee; + char h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); h_fee = getl(b, T_FEE); + 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, 'MDR', :h_fee, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + OK(b); +} + +/* ST_NETTING - 당일 net 합계 조회. */ +void ST_NETTING(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char merch[64], bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_sum; + 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_sum FROM settlement + WHERE merchant_id = :h_merch AND biz_date = :h_bizdate; + if (sqlca.sqlcode < 0) FAIL(b); + setl(b, T_GROSS, h_sum); + OK(b); +} + +/* ST_VAT - 정산 부가세 원장 기록. */ +void ST_VAT(TPSVCINFO *p) +{ + UBFH *b = (UBFH *)p->data; + char bizdate[16]; + EXEC SQL BEGIN DECLARE SECTION; + long h_pid, h_vat; + char h_bizdate[16]; + EXEC SQL END DECLARE SECTION; + h_pid = getl(b, T_PURCHASE_ID); h_vat = getl(b, T_FEE) / 10; + 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, 'VAT', :h_vat, :h_bizdate); + if (sqlca.sqlcode < 0) FAIL(b); + setl(b, T_TAX, h_vat); + OK(b); +} + +/* vim: set ts=4 sw=4 et smartindent: */ diff --git a/app/ubftab/acq.fd b/app/ubftab/acq.fd index a0b73dd..b7e8079 100644 --- a/app/ubftab/acq.fd +++ b/app/ubftab/acq.fd @@ -1,13 +1,29 @@ -$/* @(#) Acquiring (카드 매입) UBF field table */ +$/* @(#) Acquiring (카드 매입) UBF field table - shared by all modules */ *base 6000 # name id type flag comment -T_MERCHANT 1 string - - -T_AMOUNT 2 long - - -T_FEE 3 long - - -T_NET 4 long - - -T_STATUS 5 string - - -T_PURCHASE_ID 6 long - - -T_BIZDATE 7 string - - -T_SETTLE_ID 8 long - - -T_MSG 9 string - - +T_MERCHANT 1 string - 가맹점 ID +T_AMOUNT 2 long - 매입 금액 +T_FEE 3 long - 수수료 +T_NET 4 long - 정산 금액 +T_STATUS 5 string - 상태 +T_PURCHASE_ID 6 long - 매입 ID +T_BIZDATE 7 string - 영업일 +T_SETTLE_ID 8 long - 정산 ID +T_MSG 9 string - 메시지 +T_CANCEL_ID 10 long - 취소 ID +T_INSTALL_N 11 long - 할부 개월 +T_TAX 12 long - 세액 +T_FX_AMT 13 long - 해외 원통화 금액 +T_COUNT 14 long - 건수 +T_REASON 15 string - 사유 +T_CATEGORY 16 string - 업종/분류 +T_ISSUER 17 string - 발급사 +T_CHANNEL 18 string - 채널 +T_CCY 19 string - 통화코드 +T_ADJ 20 long - 조정 ID +T_DELTA 21 long - 조정 금액(증감) +T_SVCNAME 22 string - 서비스명 +T_RC 23 long - 결과코드 +T_GROSS 24 long - 총액 +T_DOCNO 25 string - 문서번호 diff --git a/app/ubftab/acq.fd.h b/app/ubftab/acq.fd.h index de21306..c2badc8 100644 --- a/app/ubftab/acq.fd.h +++ b/app/ubftab/acq.fd.h @@ -1,4 +1,4 @@ -/* @(#) Acquiring (카드 매입) UBF field table */ +/* @(#) Acquiring (카드 매입) UBF field table - shared by all modules */ /* fname bfldid */ /* ----- ----- */ #define T_MERCHANT ((BFLDID32)167778161) /* number: 6001 type: string */ @@ -10,3 +10,19 @@ #define T_BIZDATE ((BFLDID32)167778167) /* number: 6007 type: string */ #define T_SETTLE_ID ((BFLDID32)33560440) /* number: 6008 type: long */ #define T_MSG ((BFLDID32)167778169) /* number: 6009 type: string */ +#define T_CANCEL_ID ((BFLDID32)33560442) /* number: 6010 type: long */ +#define T_INSTALL_N ((BFLDID32)33560443) /* number: 6011 type: long */ +#define T_TAX ((BFLDID32)33560444) /* number: 6012 type: long */ +#define T_FX_AMT ((BFLDID32)33560445) /* number: 6013 type: long */ +#define T_COUNT ((BFLDID32)33560446) /* number: 6014 type: long */ +#define T_REASON ((BFLDID32)167778175) /* number: 6015 type: string */ +#define T_CATEGORY ((BFLDID32)167778176) /* number: 6016 type: string */ +#define T_ISSUER ((BFLDID32)167778177) /* number: 6017 type: string */ +#define T_CHANNEL ((BFLDID32)167778178) /* number: 6018 type: string */ +#define T_CCY ((BFLDID32)167778179) /* number: 6019 type: string */ +#define T_ADJ ((BFLDID32)33560452) /* number: 6020 type: long */ +#define T_DELTA ((BFLDID32)33560453) /* number: 6021 type: long */ +#define T_SVCNAME ((BFLDID32)167778182) /* number: 6022 type: string */ +#define T_RC ((BFLDID32)33560455) /* number: 6023 type: long */ +#define T_GROSS ((BFLDID32)33560456) /* number: 6024 type: long */ +#define T_DOCNO ((BFLDID32)167778185) /* number: 6025 type: string */ diff --git a/db/schema.sql b/db/schema.sql index 32600bd..4137ddb 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,4 +1,4 @@ --- Card-acquiring vertical-slice schema (카드 매입/승인/정산/원장) +-- Card-acquiring schema (카드 매입/승인/정산/원장 + ac 모듈 상세 테이블) -- Loaded into PostgreSQL on first boot (docker-entrypoint-initdb.d). -- --------------------------------------------------------------------------- @@ -8,13 +8,13 @@ CREATE TABLE IF NOT EXISTS merchant ( merchant_id TEXT PRIMARY KEY, name TEXT NOT NULL, mdr_bps INTEGER NOT NULL DEFAULT 25, -- Merchant Discount Rate, basis points (0.25%) + daily_limit BIGINT NOT NULL DEFAULT 100000000, status TEXT NOT NULL DEFAULT 'ACTIVE', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- --------------------------------------------------------------------------- -- 매입 (purchase) - the acquiring transaction --- purchase_id driven by an explicit sequence so ECPG can SELECT nextval first. -- --------------------------------------------------------------------------- CREATE SEQUENCE IF NOT EXISTS purchase_seq; CREATE TABLE IF NOT EXISTS purchase ( @@ -23,8 +23,11 @@ CREATE TABLE IF NOT EXISTS purchase ( amount BIGINT NOT NULL, -- minor units (원, 정수) fee BIGINT NOT NULL, -- MDR fee net BIGINT NOT NULL, -- amount - fee - status TEXT NOT NULL, -- A=접수, M=대사완료(matched), S=정산완료(settled) + status TEXT NOT NULL, -- A=접수, M=대사완료, S=정산완료, C=취소, H=보류, V=void biz_date DATE NOT NULL, + channel TEXT NOT NULL DEFAULT 'POS', -- POS/EDC/EDI/FOREIGN + issuer TEXT NOT NULL DEFAULT 'UNKNOWN', + category TEXT NOT NULL DEFAULT 'GEN', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); @@ -41,13 +44,13 @@ CREATE TABLE IF NOT EXISTS approval ( ); -- --------------------------------------------------------------------------- --- 원장 (ledger) - double-entry style recon check written during 대사 +-- 원장 (ledger) - recon/settle entries -- --------------------------------------------------------------------------- CREATE SEQUENCE IF NOT EXISTS ledger_seq; CREATE TABLE IF NOT EXISTS ledger ( ledger_id BIGINT PRIMARY KEY, purchase_id BIGINT NOT NULL, - entry_type TEXT NOT NULL, -- RECON / SETTLE + entry_type TEXT NOT NULL, -- RECON / SETTLE / XFER / SETTLELINK amount BIGINT NOT NULL, biz_date DATE NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() @@ -67,8 +70,101 @@ CREATE TABLE IF NOT EXISTS settlement ( settled_at TIMESTAMPTZ NOT NULL DEFAULT now() ); --- Seed a couple of merchants so ACQUIRE has a valid counterparty. -INSERT INTO merchant (merchant_id, name, mdr_bps) VALUES - ('M0001', '클라로 커피', 25), - ('M0002', '포지 마트', 25) +-- --------------------------------------------------------------------------- +-- ac 모듈 상세 테이블 (취소/할부/수수료조정/원천/세금계산서/해외/EDI/미매입/집계) +-- --------------------------------------------------------------------------- +CREATE SEQUENCE IF NOT EXISTS ac_cancel_seq; +CREATE TABLE IF NOT EXISTS ac_cancel ( + cancel_id BIGINT PRIMARY KEY, + purchase_id BIGINT NOT NULL, + cancel_type TEXT NOT NULL, -- FULL / PARTIAL / REFUND / VOID + amount BIGINT NOT NULL, + reason TEXT NOT NULL DEFAULT '', + biz_date DATE NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE SEQUENCE IF NOT EXISTS ac_install_seq; +CREATE TABLE IF NOT EXISTS ac_install ( + install_id BIGINT PRIMARY KEY, + purchase_id BIGINT NOT NULL, + seq_no INTEGER NOT NULL, + months INTEGER NOT NULL, + month_amount BIGINT NOT NULL, + biz_date DATE NOT NULL +); + +CREATE SEQUENCE IF NOT EXISTS ac_feeadj_seq; +CREATE TABLE IF NOT EXISTS ac_fee_adj ( + adj_id BIGINT PRIMARY KEY, + purchase_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() +); + +CREATE SEQUENCE IF NOT EXISTS ac_wht_seq; +CREATE TABLE IF NOT EXISTS ac_wht ( + wht_id BIGINT PRIMARY KEY, + purchase_id BIGINT NOT NULL, + tax_amount BIGINT NOT NULL, + biz_date DATE NOT NULL +); + +CREATE SEQUENCE IF NOT EXISTS ac_taxinv_seq; +CREATE TABLE IF NOT EXISTS ac_tax_invoice ( + inv_id BIGINT PRIMARY KEY, + purchase_id BIGINT NOT NULL, + merchant_id TEXT NOT NULL, + supply BIGINT NOT NULL, + vat BIGINT NOT NULL, + biz_date DATE NOT NULL +); + +CREATE SEQUENCE IF NOT EXISTS ac_fx_seq; +CREATE TABLE IF NOT EXISTS ac_fx ( + fx_id BIGINT PRIMARY KEY, + purchase_id BIGINT NOT NULL, + ccy TEXT NOT NULL, + fx_amount BIGINT NOT NULL, + krw_amount BIGINT NOT NULL, + rate_bps BIGINT NOT NULL, + biz_date DATE NOT NULL +); + +CREATE SEQUENCE IF NOT EXISTS ac_edi_seq; +CREATE TABLE IF NOT EXISTS ac_edi ( + edi_id BIGINT PRIMARY KEY, + merchant_id TEXT NOT NULL, + doc_no TEXT NOT NULL, + amount BIGINT NOT NULL, + biz_date DATE NOT NULL +); + +CREATE SEQUENCE IF NOT EXISTS ac_unmatch_seq; +CREATE TABLE IF NOT EXISTS ac_unmatch ( + unmatch_id BIGINT PRIMARY KEY, + purchase_id BIGINT NOT NULL, + reason TEXT NOT NULL, + biz_date DATE NOT NULL +); + +-- 가맹점 일별 집계 (배치가 채움) +CREATE TABLE IF NOT EXISTS merchant_summary ( + 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) +); + +-- Seed merchants so ACQUIRE has a valid counterparty. +INSERT INTO merchant (merchant_id, name, mdr_bps, daily_limit) VALUES + ('M0001', '클라로 커피', 25, 100000000), + ('M0002', '포지 마트', 25, 100000000), + ('M0003', '클라로 면세점', 30, 500000000) ON CONFLICT (merchant_id) DO NOTHING; diff --git a/docs/service-catalog.md b/docs/service-catalog.md new file mode 100644 index 0000000..34ca358 --- /dev/null +++ b/docs/service-catalog.md @@ -0,0 +1,50 @@ +# 서비스 카탈로그 — acquire-core-x (2000본 구성) + +파일 2000본은 유지하되, **모듈당 온라인 서버 바이너리 1개가 그 모듈의 온라인 서비스 다수를 +`tpadvertise`**하는 실제 Tuxedo 방식으로 프로세스를 구성한다. + +## 프로세스 구성 + +| 종류 | 프로세스/파일 | 설명 | +|---|---|---| +| 모듈 온라인 서버 | 11개 서버 바이너리 = `_svr` | 각 ~30 서비스 advertise (총 ~350) | +| dbio | 링크 라이브러리 (`libdbio.a`) | 서비스가 호출하는 EXEC SQL 함수 (~300 .pgc) | +| common | 링크 라이브러리 (`libacqcommon.a`) | leaf 유틸 (~120 .c) | +| batch | ~180 독립 실행파일 (`main`) | 마감/집계, 온디맨드, XA | +| tmsrv | RM1 (PostgreSQL XA) | 2PC 조율 | + +## 모듈별 온라인 서비스 (대표, 각 32개 목표 → 총 ~350) + +- **ac 매입**: ACQUIRE, ACQ_DDC, ACQ_EDI, ACQ_EDC, ACQ_CANCEL, ACQ_CORRECT, ACQ_PARTIAL, + ACQ_INSTALL, ACQ_FOREIGN, ACQ_UNMATCH, ACQ_REPROC, ACQ_DUPCHK, ACQ_LIMITCHK, ACQ_FEEADJ, + ACQ_STATUS, ACQ_MERCHSUM, ACQ_ISSUERCLS, ACQ_WHT, ACQ_TAXINV, ACQ_APPRLINK, ACQ_DATECHG, + ACQ_AMTFIX, ACQ_RECLASS, ACQ_XFERLINK, ACQ_APPRMAP, ACQ_SETTLELINK, … +- **au 승인/한도**: AUTH, LIMIT_CHK, LIMIT_DEC, LIMIT_RST, STANDIN, AUTH_CANCEL, AUTH_PARTCXL, + FRAUD_DETECT, AUTH_RETRY, INSTALL_AUTH, AUTH_STATUS, … +- **rc 대사**: RECONCILE, RC_APPR_ACQ, RC_ACQ_DEP, RC_RETURN, RC_AMTDIFF, RC_CNTDIFF, + RC_3WAY, RC_TOLERANCE, RC_REMATCH, RC_HOLD, RC_UNMATCH, … +- **st 정산/수수료**: SETTLE, ST_MDR, ST_VANFEE, ST_RELAYFEE, ST_NETTING, ST_VAT, ST_WHT, + ST_ADJUST, ST_ADVANCE, ST_DELAYINT, ST_HOLD, ST_SPLIT, ST_PAYDATE(T+n), … +- **py 지급**: PAY_FILEGEN, PAY_RESULT, PAY_REPROC, PAY_HOLD, PAY_CANCEL, PAY_ACCTCHK, + PAY_SPLIT, PAY_INT, PAY_XFERLINK, … +- **lg 원장**: LG_POST, LG_CREDIT, LG_DEBIT, LG_DOUBLE, LG_BALCHK, LG_REVERSE, LG_CARRY, + LG_CLOSE_LINK, … +- **cl 마감**: CL_DAILY, CL_MONTHLY, CL_QUARTER, CL_SNAPSHOT, CL_RECLOSE, CL_VERIFY, … +- **mm 마스터**: MM_MERCH_REG, MM_MERCH_UPD, MM_FEERATE, MM_BIN, MM_LIMIT, MM_CODE, … +- **vl 정합성**: VL_SALES, VL_CARD, VL_LIMIT, VL_AMOUNT, VL_ANOMALY, VL_REQUIRED, … +- **mg 전문게이트웨이**: MG_RECV, MG_SEND, MG_ISO8583, MG_STAN, MG_ROUTE, MG_RESEND, … +- **cm 공통**: CM_CODE, CM_BIZDAY, CM_AMT, CM_SEQ, CM_FX, CM_LUHN, … + +## 대표 tpcall 체인 (실동작 검증 대상) + +1. **매입 체인**: `ACQUIRE` → `RECONCILE` → `SETTLE` → `LG_POST` (글로벌 XA) ← 슬라이스 검증됨 +2. **승인 체인**: `AUTH` → `LIMIT_DEC` → `FRAUD_DETECT` +3. **정산 체인**: `SETTLE` → `ST_MDR` → `ST_NETTING` → `PAY_FILEGEN` +4. **마감 배치**: `cl_daily_batch` (커서) → `CL_SNAPSHOT` → `LG_CLOSE_LINK` + +## Phase 2 검증 게이트 (all must pass) + +- 풀빌드: 모든 `.pgc` ecpg→buildserver, 모든 batch/common 컴파일 (EXIT 0) +- 전서버 부팅: `xadmin start` 후 모든 `_svr` + tmsrv `runok` +- 전서비스 advertise: `xadmin psc`에 ~350 서비스 AVAIL +- 대표 체인 4종: 클라이언트 실행 → psql 커밋 row 확인