From e1729d6edc81c651bcb70e8407268dd455ef9752 Mon Sep 17 00:00:00 2001 From: hyeongwoo Date: Thu, 23 Jul 2026 12:10:37 +0900 Subject: [PATCH] =?UTF-8?q?Wave=20H:=20Oracle=20=EA=B3=84=EB=B3=B4(As-Is)?= =?UTF-8?q?=20=EC=8B=AC=EA=B8=B0=20+=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=A1=B0=EB=A7=9D=20=EC=9E=AC=ED=8E=B8(=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=EC=BD=98=EC=86=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ■ Oracle 계보 — "원본은 Oracle 19c + Pro*C 였다" 현행 코드(ECPG/PostgreSQL)가 Oracle 시스템의 1차 이관본임을 콘크리트하게 보존. - legacy-oracle/proc/*.pc (7본): ACQUIRE/AUTH_APPROVE/ST_MDR/SETTLE/RECONCILE/ CL_DAILY/PAY_FILEGEN. 순수 Oracle Pro*C 방언 — sqlca/oraca, VARCHAR, WHENEVER SQLERROR GOTO, NVL/DECODE/SYSDATE/DUAL/ROWNUM/(+)/CONNECT BY/ .NEXTVAL/TO_DATE, PL/SQL 패키지 호출, COMMIT WORK. 컴파일 대상 아님(As-Is 원본). - legacy-oracle/plsql/*.sql (2본): PKG_SETTLE(정산), PKG_LEDGER(복식부기 기표). VARCHAR2/%TYPE/%ROWTYPE/CURSOR/EXCEPTION/RAISE_APPLICATION_ERROR. - docs/ORACLE_PROVENANCE.md: As-Is(Oracle)→현행(ECPG)→To-Be(Spring Boot) 3단 계보 + Oracle→PostgreSQL 방언 이관 매핑 15종 + "왜 DB는 PostgreSQL인가". - gen_metrics.py: legacy-oracle 실측(원본 9파일 1,827LOC, 방언 12종 히트) → 마이그레이션 난이도 화면에 "④ Oracle 계보" 패널 추가. ■ 시스템 조망 재편 — 실제 매입시스템 운영 콘솔 "마이그레이션 난이도"(Forge 관점 메타)를 시스템 조망에서 분리하고, 시스템 자체의 상태·아키텍처를 보여주는 운영 화면 신설. - 신규 "시스템 현황"(#sys): 헬스 KPI(서버11/서비스1466/미결2PC/당일매입/채널UP/ 큐적체/오류) + 아키텍처 토폴로지(채널·카드망→전문GW→승인·매입→정산·지급→ 원장·마감·대사·검증→마스터·공통 계층도) + 채널상태 + 큐적체 + 처리량·오류율 + 정산사이클(마감·순액정산·클리어링·프레젠트먼트). 전부 실측 조회. - nav 재편: [시스템 조망]=시스템현황·아키텍처토폴로지·정산경제, [전환 분석(Forge)]=마이그레이션난이도. - 토폴로지 모듈박스 렌더 버그(return ASI) 수정. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 9 ++ app/ui/index.html | 122 +++++++++++++- app/ui/metrics.js | 2 +- docs/ORACLE_PROVENANCE.md | 69 ++++++++ legacy-oracle/plsql/PKG_LEDGER.sql | 182 +++++++++++++++++++++ legacy-oracle/plsql/PKG_SETTLE.sql | 214 +++++++++++++++++++++++++ legacy-oracle/proc/ACQUIRE.pc | 177 +++++++++++++++++++++ legacy-oracle/proc/AUTH_APPROVE.pc | 234 +++++++++++++++++++++++++++ legacy-oracle/proc/CL_DAILY.pc | 230 +++++++++++++++++++++++++++ legacy-oracle/proc/PAY_FILEGEN.pc | 247 +++++++++++++++++++++++++++++ legacy-oracle/proc/RECONCILE.pc | 224 ++++++++++++++++++++++++++ legacy-oracle/proc/SETTLE.pc | 162 +++++++++++++++++++ legacy-oracle/proc/ST_MDR.pc | 157 ++++++++++++++++++ tools/gen_metrics.py | 32 +++- 14 files changed, 2054 insertions(+), 7 deletions(-) create mode 100644 docs/ORACLE_PROVENANCE.md create mode 100644 legacy-oracle/plsql/PKG_LEDGER.sql create mode 100644 legacy-oracle/plsql/PKG_SETTLE.sql create mode 100644 legacy-oracle/proc/ACQUIRE.pc create mode 100644 legacy-oracle/proc/AUTH_APPROVE.pc create mode 100644 legacy-oracle/proc/CL_DAILY.pc create mode 100644 legacy-oracle/proc/PAY_FILEGEN.pc create mode 100644 legacy-oracle/proc/RECONCILE.pc create mode 100644 legacy-oracle/proc/SETTLE.pc create mode 100644 legacy-oracle/proc/ST_MDR.pc diff --git a/README.md b/README.md index aacfb95..ed5860b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,15 @@ > 실전 레거시성)으로 **전량 실측**해 정리. 포털에서도 실시간 확인: > `#arch`(호출그래프 552노드·41 모듈간 경로), `#mig`(마이그레이션 난이도 브리핑). > 지표는 전부 소스에서 자동 집계된다(`tools/gen_callgraph.py`, `tools/gen_metrics.py`). +> +> **Oracle 계보(As-Is)** — [`docs/ORACLE_PROVENANCE.md`](docs/ORACLE_PROVENANCE.md): +> 원본은 Oracle 19c + Pro\*C 였고, 현행 코드는 그 1차 이관본(ECPG/PostgreSQL)이다. +> `legacy-oracle/` 에 As-Is Pro\*C/PL-SQL 역사 원본을 보존(컴파일 제외)하고, Oracle→ +> PostgreSQL 방언 이관 매핑을 문서화한다. +> +> **포털 화면 구성** — `#sys`(시스템 현황: 상태·아키텍처 토폴로지·채널·큐·정산사이클), +> `#arch`(아키텍처 토폴로지), `#econ`(정산 경제: 수수료 3층·펀딩·순액정산·MCC·ISO8583), +> `#mig`(마이그레이션 난이도, Forge 관점). --- diff --git a/app/ui/index.html b/app/ui/index.html index ea71484..3ef0763 100644 --- a/app/ui/index.html +++ b/app/ui/index.html @@ -123,7 +123,7 @@
상세
diff --git a/app/ui/metrics.js b/app/ui/metrics.js index 37f2543..34ff4e7 100644 --- a/app/ui/metrics.js +++ b/app/ui/metrics.js @@ -1,2 +1,2 @@ /* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */ -window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4640,"u":"개"},{"k":"총 코드 라인","v":331856,"u":"LOC"},{"k":"온라인 서비스","v":1470,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23817,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47127,"u":"곳"},{"k":"DB 테이블","v":86,"u":"개"},{"k":"카피북 헤더(.h)","v":2214,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":989,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23817,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47127,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6906,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2254,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2214,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2254,"goto":64,"userlog":17185,"sqlca_checks":18795,"embedded_sql_density":9.8}}; +window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4640,"u":"개"},{"k":"총 코드 라인","v":331856,"u":"LOC"},{"k":"온라인 서비스","v":1470,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23817,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":47127,"u":"곳"},{"k":"DB 테이블","v":86,"u":"개"},{"k":"카피북 헤더(.h)","v":2214,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":989,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23817,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":47127,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6906,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2254,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2214,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2254,"goto":64,"userlog":17185,"sqlca_checks":18795,"embedded_sql_density":9.8},"ORACLE":{"asis_files":9,"asis_proc":7,"asis_plsql":2,"asis_loc":1827,"dialects":[{"t":"PL/SQL 패키지 호출 (pkg_x.proc)","n":18},{"t":"NVL / NVL2 널가드","n":60},{"t":"DECODE 다분기","n":16},{"t":"SYSDATE / SYSTIMESTAMP","n":42},{"t":"FROM DUAL","n":15},{"t":"시퀀스 .NEXTVAL","n":27},{"t":"ROWNUM 의사컬럼","n":6},{"t":"구식 외부조인 (+)","n":8},{"t":"CONNECT BY 계층질의","n":3},{"t":"WHENEVER SQLERROR 라벨분기","n":16},{"t":"TO_DATE / TO_CHAR 포맷","n":35},{"t":"RAISE_APPLICATION_ERROR","n":13}]}}; diff --git a/docs/ORACLE_PROVENANCE.md b/docs/ORACLE_PROVENANCE.md new file mode 100644 index 0000000..d5c4614 --- /dev/null +++ b/docs/ORACLE_PROVENANCE.md @@ -0,0 +1,69 @@ +# Oracle 계보 (As-Is) — acquire-core-x + +> **이 시스템의 원본은 Oracle 이다.** BC 매입시스템 As-Is 는 Oracle 19c + Pro\*C + +> Tuxedo/ATMI 였고(BAIS RFI 기준), 본 저장소의 현행 코드는 그 시스템을 오픈 스택 +> (Enduro/X + ECPG + PostgreSQL)으로 **이식한 1차 이관본**이다. Klaro Forge 가 +> 수행할 다음 이관은 이 레거시 → Java Spring Boot 다. +> +> `legacy-oracle/` 에 **As-Is Oracle Pro\*C 역사 원본**을 보존한다(컴파일 대상 아님). +> 현행 이식본(`app/src/**/*.pgc`, ECPG/PostgreSQL)과 1:1로 대조 가능하다. + +## 3단 계보 + +``` + ┌── As-Is ───────────┐ ┌── 현행(1차 이관본) ─────┐ ┌── To-Be (Forge) ──────┐ + │ Oracle 19c │ │ PostgreSQL 16 │ │ PostgreSQL 16 │ + │ Pro*C (proc) │ → │ ECPG (EXEC SQL) │ → │ JPA / MyBatis │ + │ Tuxedo/ATMI │ │ Enduro/X (open ATMI) │ │ Spring Boot 3.x │ + │ PL/SQL 패키지 │ │ (패키지→서비스 인라인) │ │ @Service / @Transactional + │ legacy-oracle/*.pc │ │ app/src/**/*.pgc │ │ (target) │ + └─────────────────────┘ └─────────────────────────┘ └────────────────────────┘ +``` + +- As-Is → 현행: **DB 엔진 이관**(Oracle→PostgreSQL)은 끝났다. Oracle 방언·Pro\*C·PL/SQL + 패키지를 PostgreSQL/ECPG 로 옮긴 결과가 현행 코드다. +- 현행 → To-Be: 남은 난제는 **애플리케이션 계층**(ATMI·임베디드SQL·UBF·XA)이며, + 이게 Forge 가 다룰 대상이다. DB 엔진은 이미 PostgreSQL 이라 To-Be 와 일치한다. + +## Oracle → PostgreSQL 방언 이관 매핑 (1차 이관에서 실제로 옮긴 것들) + +| Oracle (As-Is, Pro\*C) | PostgreSQL (현행, ECPG) | 이관 난이도·주의 | +|---|---|---| +| `VARCHAR2(n)` / Pro\*C `VARCHAR h[n]` | `TEXT` / `char h[n]` | 널·공백 트림 의미 차이 | +| `NVL(x, d)` | `COALESCE(x, d)` | 인자 개수 차이(NVL2→CASE) | +| `DECODE(e, k1,v1, k2,v2, d)` | `CASE WHEN e=k1 THEN v1 ... ELSE d END` | 다분기 전개, NULL 동등성 차이 | +| `SYSDATE` / `SYSTIMESTAMP` | `now()` / `current_date` | 시간대·타입 차이 | +| `... FROM DUAL` | (FROM 절 생략) | 시퀀스·스칼라 조회 재작성 | +| `seq.NEXTVAL` / `seq.CURRVAL` | `nextval('seq')` / `currval('seq')` | 문법·세션 시맨틱 차이 | +| `ROWNUM = 1` / `ROWNUM <= n` | `LIMIT 1` / `LIMIT n` | ROWNUM 은 정렬 前 적용 → 의미 함정 | +| 외부조인 `t1.a (+) = t2.b` | `LEFT/RIGHT JOIN ... ON` | 구식 `(+)` 전면 재작성 | +| `CONNECT BY PRIOR` 계층질의 | `WITH RECURSIVE` CTE | 계층 재귀 완전 재작성(최난이도) | +| `TO_DATE(s,'YYYYMMDD')` / `TO_CHAR(d,fmt)` | `to_date`/`to_char` (포맷토큰 일부 상이) | 포맷 마스크 검증 필요 | +| `(+)`/`MINUS`/`ROWID` | `EXCEPT`/`ctid`(비권장) | 세트연산·행식별자 대체 | +| Pro\*C `EXEC SQL WHENEVER SQLERROR GOTO` | ECPG 동일 문 + `sqlca.sqlcode` 분기 | 라벨분기 유지 가능 | +| Pro\*C `EXEC SQL INCLUDE oraca` / `EXEC ORACLE OPTION` | (해당 없음) | Oracle 전용 진단영역 제거 | +| **PL/SQL 패키지** (`pkg_x.proc`) | ECPG 서비스 인라인 / 함수 | 패키지 상태·오버로드 소실, 로직 서비스로 흡수 | +| `RAISE_APPLICATION_ERROR(-20001,...)` | `RAISE EXCEPTION` / 리턴코드 | 예외 프로토콜 재설계 | +| `UTL_FILE` 파일 I/O | 서버 파일 I/O / COPY | 배치 파일생성 재작성 | + +## As-Is 원본 보존 목록 (`legacy-oracle/`) + +| 원본 (Oracle Pro\*C / PL-SQL) | 현행 이식본 (ECPG) | 업무 | +|---|---|---| +| `proc/ACQUIRE.pc` | `app/src/ac/svc/ACQUIRE.pgc` | 매입 접수 | +| `proc/AUTH_APPROVE.pc` | `app/src/au/svc/AUTH_APPROVE.pgc` | 승인 확정 | +| `proc/ST_MDR.pc` | `app/src/st/svc/ST_MDR.pgc` | MDR 수수료 기표 | +| `proc/SETTLE.pc` | `app/src/st/svc/SETTLE.pgc` | 정산 확정 | +| `proc/RECONCILE.pc` | `app/src/rc/svc/RECONCILE.pgc` | 대사 (CONNECT BY) | +| `proc/CL_DAILY.pc` | `app/src/cl/svc/CL_DAILY.pgc` | 일마감 배치 | +| `proc/PAY_FILEGEN.pc` | `app/src/py/svc/PAY_FILEGEN.pgc` | 지급파일 생성 배치 | +| `plsql/PKG_SETTLE.sql` | (서비스로 인라인) | 정산 PL/SQL 패키지 | +| `plsql/PKG_LEDGER.sql` | (서비스로 인라인) | 원장 기표 PL/SQL 패키지 | + +## 왜 DB 는 PostgreSQL 인가 (Oracle 로 되돌리지 않는 이유) + +- **To-Be 가 PostgreSQL** 이다(BAIS RFI: Oracle 19c → PostgreSQL 16). DB 이관은 이미 + 완료 상태로 두고, Forge 가 다룰 **애플리케이션 이관**(ATMI/임베디드SQL/UXA)에 집중한다. +- 라이선스·재현성: 오픈 스택이라 `docker compose up` 한 번으로 어디서든 실동작. +- 마이그레이션 난이도의 본질은 DB 엔진이 아니라 그 위의 Pro\*C·Tuxedo·XA 계층이며, + 그 난이도는 위 원본들에서 그대로 확인된다. diff --git a/legacy-oracle/plsql/PKG_LEDGER.sql b/legacy-oracle/plsql/PKG_LEDGER.sql new file mode 100644 index 0000000..6e4b201 --- /dev/null +++ b/legacy-oracle/plsql/PKG_LEDGER.sql @@ -0,0 +1,182 @@ +--========================================================================== +-- PKG_LEDGER.sql - 원장 복식부기 기표 패키지 (As-Is 원본, Oracle PL/SQL) +---------------------------------------------------------------------------- +-- 대상 : BC 매입시스템 / 회계 원장 도메인 공용 패키지 PKG_LEDGER +-- DBMS : Oracle 19c +-- ※ 이 파일은 As-Is 역사 원본이다. pkg_settle 및 ACQUIRE/SETTLE/ST_MDR +-- 계열이 호출하는 복식부기(차/대변) 기표 엔진이다. 현행 오픈스택 +-- 이식본에서는 ledger 단식 INSERT (app/src/st/svc/*, LG_* 서비스) 로 +-- 단순화되었다. 컴파일 대상 아님(빌드 제외). +---------------------------------------------------------------------------- +-- 변경이력 +-- 1998.12.05 KIM K.H 최초작성 (전표 헤더/명세 복식부기 기표) +-- 2007.06.18 PARK J.S 전표유형->계정 매핑 DECODE 테이블 도입 +-- 2014.02.27 LEE S.W 전표 역분개 reverse_voucher, 차대 균형검증 강화 +-- 2019.09.01 CHOI H.W Oracle 11g -> 19c, 예외코드 -20600 대역 정리 +--========================================================================== + +CREATE OR REPLACE PACKAGE pkg_ledger AS + + -- 차/대변 구분 + C_DEBIT CONSTANT VARCHAR2(1) := 'D'; -- 차변 + C_CREDIT CONSTANT VARCHAR2(1) := 'C'; -- 대변 + + -- 사용자정의 예외 (-20600 대역) + e_unbalanced EXCEPTION; + e_bad_type EXCEPTION; + e_no_voucher EXCEPTION; + PRAGMA EXCEPTION_INIT(e_unbalanced, -20601); + PRAGMA EXCEPTION_INIT(e_bad_type, -20602); + PRAGMA EXCEPTION_INIT(e_no_voucher, -20603); + + -- 전표유형 -> (차변계정, 대변계정) 매핑 + PROCEDURE map_accounts(p_type IN VARCHAR2, + o_dr_acct OUT VARCHAR2, + o_cr_acct OUT VARCHAR2); + + -- 단일 전표 복식부기 기표 (헤더 1 + 명세 2: 차변/대변) + PROCEDURE post_voucher(p_pid IN NUMBER, + p_type IN VARCHAR2, + p_amount IN NUMBER, + p_bizdate IN DATE); + + -- 전표 역분개 (차대 반대로 재기표) + PROCEDURE reverse_voucher(p_voucher_id IN NUMBER, p_reason IN VARCHAR2); + + -- 전표 차대 균형 검증 (미균형시 예외) + FUNCTION is_balanced(p_voucher_id IN NUMBER) RETURN BOOLEAN; + +END pkg_ledger; +/ + +CREATE OR REPLACE PACKAGE BODY pkg_ledger AS + + ---------------------------------------------------------------------- + -- map_accounts : 전표유형별 차변/대변 계정 결정 (DECODE 매핑) + ---------------------------------------------------------------------- + PROCEDURE map_accounts(p_type IN VARCHAR2, + o_dr_acct OUT VARCHAR2, + o_cr_acct OUT VARCHAR2) + IS + BEGIN + -- 매입채권(1101)/미지급금(2101)/수수료수익(4101)/현금(1001) + o_dr_acct := DECODE(p_type, + 'MDR', '1101', -- 매입채권 / 수수료수익 + 'SETTLE', '1101', -- 매입채권 / 정산미지급 + 'PAYOUT', '2101', -- 정산미지급 / 현금 + 'SETTLE_REV', '2101', -- (역) 정산미지급 / 매입채권 + NULL); + o_cr_acct := DECODE(p_type, + 'MDR', '4101', + 'SETTLE', '2101', + 'PAYOUT', '1001', + 'SETTLE_REV', '1101', + NULL); + IF o_dr_acct IS NULL OR o_cr_acct IS NULL THEN + RAISE_APPLICATION_ERROR(-20602, '미정의 전표유형 type='||p_type); + END IF; + END map_accounts; + + ---------------------------------------------------------------------- + -- post_voucher : 복식부기 전표 1건 기표 + ---------------------------------------------------------------------- + PROCEDURE post_voucher(p_pid IN NUMBER, + p_type IN VARCHAR2, + p_amount IN NUMBER, + p_bizdate IN DATE) + IS + v_vid ledger_voucher.voucher_id%TYPE; + v_dr VARCHAR2(8); + v_cr VARCHAR2(8); + v_abs NUMBER := ABS(p_amount); + v_side VARCHAR2(1); + BEGIN + map_accounts(p_type, v_dr, v_cr); + + -- 음수 전표(역기표)는 차/대변을 교차 + IF p_amount < 0 THEN + v_side := v_dr; v_dr := v_cr; v_cr := v_side; + END IF; + + v_vid := ledger_voucher_seq.NEXTVAL; + INSERT INTO ledger_voucher + (voucher_id, purchase_id, entry_type, amount, biz_date, status, created_at) + VALUES (v_vid, p_pid, p_type, p_amount, p_bizdate, 'N', SYSDATE); + + -- 명세 2행: 차변 / 대변 + INSERT INTO ledger_entry + (entry_id, voucher_id, account_cd, dc_flag, amount, biz_date) + VALUES (ledger_entry_seq.NEXTVAL, v_vid, v_dr, C_DEBIT, v_abs, p_bizdate); + INSERT INTO ledger_entry + (entry_id, voucher_id, account_cd, dc_flag, amount, biz_date) + VALUES (ledger_entry_seq.NEXTVAL, v_vid, v_cr, C_CREDIT, v_abs, p_bizdate); + + -- 균형 검증 (방어적) + IF NOT is_balanced(v_vid) THEN + RAISE_APPLICATION_ERROR(-20601, '전표 차대 불균형 vid='||v_vid); + END IF; + END post_voucher; + + ---------------------------------------------------------------------- + -- reverse_voucher : 기존 전표를 역분개 + ---------------------------------------------------------------------- + PROCEDURE reverse_voucher(p_voucher_id IN NUMBER, p_reason IN VARCHAR2) + IS + r_vou ledger_voucher%ROWTYPE; + CURSOR c_ent(cp_vid NUMBER) IS + SELECT account_cd, dc_flag, amount + FROM ledger_entry + WHERE voucher_id = cp_vid; + v_newvid ledger_voucher.voucher_id%TYPE; + v_flip VARCHAR2(1); + BEGIN + SELECT * INTO r_vou + FROM ledger_voucher + WHERE voucher_id = p_voucher_id + FOR UPDATE OF status; + + IF r_vou.status = 'R' THEN + RAISE_APPLICATION_ERROR(-20603, '이미 역분개 vid='||p_voucher_id); + END IF; + + v_newvid := ledger_voucher_seq.NEXTVAL; + INSERT INTO ledger_voucher + (voucher_id, purchase_id, entry_type, amount, biz_date, status, ref_voucher, created_at) + VALUES (v_newvid, r_vou.purchase_id, r_vou.entry_type||'_REV', + -r_vou.amount, TRUNC(SYSDATE), 'N', p_voucher_id, SYSDATE); + + -- 원전표 명세를 차대 반대로 복제 + FOR e IN c_ent(p_voucher_id) LOOP + v_flip := DECODE(e.dc_flag, C_DEBIT, C_CREDIT, C_DEBIT); + INSERT INTO ledger_entry + (entry_id, voucher_id, account_cd, dc_flag, amount, biz_date) + VALUES (ledger_entry_seq.NEXTVAL, v_newvid, e.account_cd, + v_flip, e.amount, TRUNC(SYSDATE)); + END LOOP; + + UPDATE ledger_voucher + SET status = 'R', reverse_rsn = p_reason, reversed_at = SYSDATE + WHERE voucher_id = p_voucher_id; + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE_APPLICATION_ERROR(-20603, '전표 없음 vid='||p_voucher_id); + END reverse_voucher; + + ---------------------------------------------------------------------- + -- is_balanced : 전표 차변합 = 대변합 여부 + ---------------------------------------------------------------------- + FUNCTION is_balanced(p_voucher_id IN NUMBER) RETURN BOOLEAN + IS + v_dr NUMBER := 0; + v_cr NUMBER := 0; + BEGIN + SELECT NVL(SUM(DECODE(dc_flag, C_DEBIT, amount, 0)), 0), + NVL(SUM(DECODE(dc_flag, C_CREDIT, amount, 0)), 0) + INTO v_dr, v_cr + FROM ledger_entry + WHERE voucher_id = p_voucher_id; + RETURN (v_dr = v_cr AND v_dr > 0); + END is_balanced; + +END pkg_ledger; +/ diff --git a/legacy-oracle/plsql/PKG_SETTLE.sql b/legacy-oracle/plsql/PKG_SETTLE.sql new file mode 100644 index 0000000..6131ea0 --- /dev/null +++ b/legacy-oracle/plsql/PKG_SETTLE.sql @@ -0,0 +1,214 @@ +--========================================================================== +-- PKG_SETTLE.sql - 정산 처리 패키지 (As-Is 원본, Oracle PL/SQL) +---------------------------------------------------------------------------- +-- 대상 : BC 매입시스템 / 정산 도메인 공용 패키지 PKG_SETTLE +-- DBMS : Oracle 19c +-- ※ 이 파일은 As-Is 역사 원본이다. ACQUIRE.pc 의 +-- "pkg_settle.acquire_post(:h_pid,:h_merch,:h_amount,:h_fee,:h_net)" +-- 호출 대상이며, 현행 오픈스택 이식본에서는 SETTLE/ST_MDR 서비스 +-- (app/src/st/svc/*) 로 인라인 전개되었다. 컴파일 대상 아님(빌드 제외). +---------------------------------------------------------------------------- +-- 변경이력 +-- 1998.11.20 KIM K.H 최초작성 (매입시점 정산예정 적재) +-- 2005.03.14 PARK J.S 가맹점 일단위 묶음정산 settle_merchant_day 추가 +-- 2012.09.01 LEE S.W 정산확정/취소 프로시저 분리, 원장연계 pkg_ledger +-- 2019.09.01 CHOI H.W Oracle 11g -> 19c, 예외코드 -20500 대역 정리 +--========================================================================== + +CREATE OR REPLACE PACKAGE pkg_settle AS + + -- 정산 상태 상수 + C_PENDING CONSTANT VARCHAR2(1) := 'P'; -- 정산예정 + C_SETTLED CONSTANT VARCHAR2(1) := 'S'; -- 정산확정 + C_CANCELED CONSTANT VARCHAR2(1) := 'X'; -- 정산취소 + + -- 사용자정의 예외 (-20500 대역) + e_bad_amount EXCEPTION; + e_no_purchase EXCEPTION; + e_already_set EXCEPTION; + PRAGMA EXCEPTION_INIT(e_bad_amount, -20501); + PRAGMA EXCEPTION_INIT(e_no_purchase, -20502); + PRAGMA EXCEPTION_INIT(e_already_set, -20503); + + -- ACQUIRE.pc 가 매입확정 직후 호출: 정산예정행 + 원장기표 + PROCEDURE acquire_post(p_pid IN NUMBER, + p_merch IN VARCHAR2, + p_amount IN NUMBER, + p_fee IN NUMBER, + p_net IN NUMBER); + + -- 가맹점 일단위 묶음정산 (커서 집계 -> settlement 확정) + PROCEDURE settle_merchant_day(p_merch IN VARCHAR2, + p_bizdate IN DATE, + o_sid OUT NUMBER, + o_net OUT NUMBER); + + -- 정산 확정 (P -> S) : 원장 정산기표 연계 + PROCEDURE confirm_settlement(p_sid IN NUMBER); + + -- 정산 취소 (S/P -> X) : 역기표 + PROCEDURE cancel_settlement(p_sid IN NUMBER, p_reason IN VARCHAR2); + +END pkg_settle; +/ + +CREATE OR REPLACE PACKAGE BODY pkg_settle AS + + ---------------------------------------------------------------------- + -- acquire_post : 매입 1건 정산예정 적재 + MDR/정산 원장기표 + ---------------------------------------------------------------------- + PROCEDURE acquire_post(p_pid IN NUMBER, + p_merch IN VARCHAR2, + p_amount IN NUMBER, + p_fee IN NUMBER, + p_net IN NUMBER) + IS + v_sid settlement.settlement_id%TYPE; + v_bizdate purchase.biz_date%TYPE; + v_cnt PLS_INTEGER; + BEGIN + IF p_amount <= 0 OR p_net < 0 THEN + RAISE_APPLICATION_ERROR(-20501, + '정산 금액 오류 amount='||p_amount||' net='||p_net); + END IF; + + -- 매입행 존재/영업일 확인 + BEGIN + SELECT biz_date INTO v_bizdate + FROM purchase + WHERE purchase_id = p_pid; + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE_APPLICATION_ERROR(-20502, + '매입원장 없음 pid='||p_pid); + END; + + -- 중복 정산예정 방지 + SELECT COUNT(*) INTO v_cnt + FROM settlement + WHERE purchase_id = p_pid + AND status <> C_CANCELED; + IF v_cnt > 0 THEN + RAISE_APPLICATION_ERROR(-20503, + '이미 정산예정 존재 pid='||p_pid); + END IF; + + v_sid := settlement_seq.NEXTVAL; + INSERT INTO settlement + (settlement_id, purchase_id, merchant_id, + net, fee, biz_date, status, created_at) + VALUES (v_sid, p_pid, p_merch, + p_net, p_fee, v_bizdate, C_PENDING, SYSDATE); + + -- 매입시점 원장기표 : 수수료(MDR) + 정산부채 + pkg_ledger.post_voucher(p_pid, 'MDR', p_fee, v_bizdate); + pkg_ledger.post_voucher(p_pid, 'SETTLE', p_net, v_bizdate); + END acquire_post; + + ---------------------------------------------------------------------- + -- settle_merchant_day : 가맹점 당일 정산예정 묶음 -> 확정 + ---------------------------------------------------------------------- + PROCEDURE settle_merchant_day(p_merch IN VARCHAR2, + p_bizdate IN DATE, + o_sid OUT NUMBER, + o_net OUT NUMBER) + IS + CURSOR c_pend IS + SELECT s.settlement_id, s.purchase_id, s.net + FROM settlement s + WHERE s.merchant_id = p_merch + AND s.biz_date = p_bizdate + AND s.status = C_PENDING + FOR UPDATE OF s.status; + r_pend c_pend%ROWTYPE; + v_sum NUMBER := 0; + v_cnt PLS_INTEGER := 0; + BEGIN + OPEN c_pend; + LOOP + FETCH c_pend INTO r_pend; + EXIT WHEN c_pend%NOTFOUND; + v_sum := v_sum + NVL(r_pend.net, 0); + v_cnt := v_cnt + 1; + UPDATE settlement + SET status = C_SETTLED, settled_at = SYSDATE + WHERE CURRENT OF c_pend; + END LOOP; + CLOSE c_pend; + + IF v_cnt = 0 THEN + RAISE_APPLICATION_ERROR(-20502, + '정산예정 없음 merch='||p_merch||' date='||TO_CHAR(p_bizdate,'YYYYMMDD')); + END IF; + + o_sid := settle_batch_seq.NEXTVAL; + o_net := v_sum; + INSERT INTO settle_batch + (batch_id, merchant_id, biz_date, txn_count, net_amount, status, created_at) + VALUES (o_sid, p_merch, p_bizdate, v_cnt, v_sum, C_SETTLED, SYSDATE); + END settle_merchant_day; + + ---------------------------------------------------------------------- + -- confirm_settlement : 개별 정산예정 확정 + ---------------------------------------------------------------------- + PROCEDURE confirm_settlement(p_sid IN NUMBER) + IS + r_set settlement%ROWTYPE; + BEGIN + SELECT * INTO r_set + FROM settlement + WHERE settlement_id = p_sid + FOR UPDATE OF status; + + IF r_set.status = C_SETTLED THEN + RAISE_APPLICATION_ERROR(-20503, '이미 확정 sid='||p_sid); + ELSIF r_set.status = C_CANCELED THEN + RAISE_APPLICATION_ERROR(-20503, '취소건 확정불가 sid='||p_sid); + END IF; + + UPDATE settlement + SET status = C_SETTLED, settled_at = SYSDATE + WHERE settlement_id = p_sid; + + pkg_ledger.post_voucher(r_set.purchase_id, 'PAYOUT', + r_set.net, r_set.biz_date); + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE_APPLICATION_ERROR(-20502, '정산건 없음 sid='||p_sid); + END confirm_settlement; + + ---------------------------------------------------------------------- + -- cancel_settlement : 정산 취소 + 역기표 + ---------------------------------------------------------------------- + PROCEDURE cancel_settlement(p_sid IN NUMBER, p_reason IN VARCHAR2) + IS + v_pid settlement.purchase_id%TYPE; + v_net settlement.net%TYPE; + v_bd settlement.biz_date%TYPE; + v_st settlement.status%TYPE; + BEGIN + SELECT purchase_id, net, biz_date, status + INTO v_pid, v_net, v_bd, v_st + FROM settlement + WHERE settlement_id = p_sid + FOR UPDATE OF status; + + IF v_st = C_CANCELED THEN + RAISE_APPLICATION_ERROR(-20503, '이미 취소 sid='||p_sid); + END IF; + + UPDATE settlement + SET status = C_CANCELED, + cancel_rsn = p_reason, + canceled_at = SYSDATE + WHERE settlement_id = p_sid; + + -- 역기표 (음수 정산) + pkg_ledger.post_voucher(v_pid, 'SETTLE_REV', -v_net, v_bd); + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE_APPLICATION_ERROR(-20502, '정산건 없음 sid='||p_sid); + END cancel_settlement; + +END pkg_settle; +/ diff --git a/legacy-oracle/proc/ACQUIRE.pc b/legacy-oracle/proc/ACQUIRE.pc new file mode 100644 index 0000000..9c449b1 --- /dev/null +++ b/legacy-oracle/proc/ACQUIRE.pc @@ -0,0 +1,177 @@ +/*==========================================================================*/ +/* ACQUIRE.pc - 매입 접수 온라인 서비스 (As-Is 원본, Oracle Pro*C) */ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / Tuxedo ATMI 서비스 ACQUIRE */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/ac/svc/ACQUIRE.pgc (ECPG/PostgreSQL). 대조는 */ +/* docs/ORACLE_PROVENANCE.md 참조. 컴파일 대상 아님(빌드 제외). */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 1998.11.02 KIM K.H 최초작성 (신용승인 연계 매입) */ +/* 2004.06.18 PARK J.S 체크카드 매입구분 추가, DECODE 요율분기 */ +/* 2011.03.30 LEE S.W 원천징수(3.3%) 반영, 세금계산서 연계 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, CHAR->VARCHAR2 정비 */ +/*==========================================================================*/ +#include +#include +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +/* 전역 FML 필드 ID (FLD.h 에서 생성) */ +#include "acqfld.h" + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_merch[16]; + VARCHAR h_name[64]; + VARCHAR h_status[2]; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_cardtype[2]; /* C=신용 K=체크 */ + long h_amount; + long h_limit; + long h_daysum; + long h_daycnt; + long h_mdr_bps; + long h_fee; + long h_net; + long h_wht; /* 원천징수액 */ + long h_pid; + long h_grade; +EXEC SQL END DECLARE SECTION; + +/*--------------------------------------------------------------------------*/ +/* ACQUIRE - Tuxedo 서비스 엔트리 */ +/*--------------------------------------------------------------------------*/ +void ACQUIRE(TPSVCINFO *rqst) +{ + FBFR32 *fb = (FBFR32 *)rqst->data; + char merch[16], bizdate[9]; + long amount; + + userlog("ACQUIRE 진입: 매입 접수 요청"); + + /* Pro*C 오류는 즉시 라벨분기 (전형적 레거시 패턴) */ + EXEC SQL WHENEVER SQLERROR GOTO sql_error; + + /* ---- 1) 요청 FML 파싱 -------------------------------------------- */ + Fget(fb, ACQ_MERCHANT, 0, merch, (FLDLEN32 *)NULL); + Fget(fb, ACQ_BIZDATE, 0, bizdate, (FLDLEN32 *)NULL); + amount = Fvall(fb, ACQ_AMOUNT, 0); + + strcpy((char *)h_merch.arr, merch); h_merch.len = strlen(merch); + strcpy((char *)h_bizdate.arr, bizdate); h_bizdate.len = strlen(bizdate); + h_amount = amount; + + if (h_amount <= 0) { + Fchg(fb, ACQ_RC, 0, (char *)"E101", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + + /* 영업일 미지정시 시스템일자 (Oracle SYSDATE) */ + if (h_bizdate.len == 0) { + EXEC SQL SELECT TO_CHAR(SYSDATE,'YYYYMMDD') INTO :h_bizdate FROM DUAL; + } + + /* ---- 2) 가맹점 마스터 조회 (NVL 로 널가드) ----------------------- */ + EXEC SQL + SELECT m.name, + NVL(m.status,'N'), + NVL(m.daily_limit, 0), + NVL(m.mdr_bps, 25), + DECODE(m.risk_grade, 'A',1, 'B',2, 'C',3, 9) + INTO :h_name, :h_status, :h_limit, :h_mdr_bps, :h_grade + FROM merchant m + WHERE m.merchant_id = :h_merch; + + if (strncmp((char *)h_status.arr, "A", 1) != 0) { + userlog("ACQUIRE: 비활성 가맹점 %s status=%.1s", merch, h_status.arr); + Fchg(fb, ACQ_RC, 0, (char *)"E203", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 3) 당일 누적 매입 조회 (한도 점검용) ------------------------ */ + EXEC SQL + SELECT COUNT(*), NVL(SUM(amount), 0) + INTO :h_daycnt, :h_daysum + FROM purchase + WHERE merchant_id = :h_merch + AND biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND status <> 'C'; + + if (h_daysum + h_amount > h_limit) { + userlog("ACQUIRE: 일한도 초과 merch=%s sum=%ld+%ld > %ld", + merch, h_daysum, h_amount, h_limit); + Fchg(fb, ACQ_RC, 0, (char *)"E210", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 4) 수수료/원천/정산액 산출 (DECODE 카드구분 요율) ----------- */ + /* 카드종류는 승인원장 조인으로 (외부조인 (+) — 무승인 매입 허용) */ + EXEC SQL + SELECT NVL(a.card_type, 'C') + INTO :h_cardtype + FROM au_authorization a + WHERE a.auth_id (+) = :h_pid; + + h_fee = h_amount * h_mdr_bps / 10000; + /* 병의원/약국(원천징수 대상 업종)만 3.3% 원천 (DECODE 업종분기) */ + EXEC SQL + SELECT DECODE(mcc.risk_level, 1, ROUND(:h_amount * 33 / 1000), 0) + INTO :h_wht + FROM mm_merch_mcc mm, mm_mcc mcc + WHERE mm.merchant_id = :h_merch + AND mcc.mcc = mm.mcc + AND ROWNUM = 1; + + h_net = h_amount - h_fee - h_wht; + + /* ---- 5) 매입원장 채번 및 적재 (시퀀스 .NEXTVAL) ------------------ */ + EXEC SQL SELECT purchase_seq.NEXTVAL INTO :h_pid FROM DUAL; + + EXEC SQL + INSERT INTO purchase + (purchase_id, merchant_id, amount, fee, net, wht, + status, biz_date, created_at) + VALUES (:h_pid, :h_merch, :h_amount, :h_fee, :h_net, :h_wht, + 'A', TO_DATE(:h_bizdate,'YYYYMMDD'), SYSDATE); + + EXEC SQL + INSERT INTO approval (approval_id, purchase_id, approved, approved_at) + VALUES (approval_seq.NEXTVAL, :h_pid, 'Y', SYSDATE); + + /* ---- 6) 정산 PL/SQL 패키지 호출 (묶음 정산 연계) ----------------- */ + EXEC SQL EXECUTE + BEGIN + pkg_settle.acquire_post(:h_pid, :h_merch, :h_amount, :h_fee, :h_net); + END; + END-EXEC; + + /* ---- 7) 커밋 및 응답 -------------------------------------------- */ + EXEC SQL COMMIT WORK; + + Fchg(fb, ACQ_PURCHASE_ID, 0, (char *)&h_pid, 0L); + Fchg(fb, ACQ_FEE, 0, (char *)&h_fee, 0L); + Fchg(fb, ACQ_NET, 0, (char *)&h_net, 0L); + Fchg(fb, ACQ_STATUS, 0, (char *)"S", 0L); + + userlog("ACQUIRE 완료: pid=%ld fee=%ld wht=%ld net=%ld", h_pid, h_fee, h_wht, h_net); + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + +sql_error: + userlog("ACQUIRE SQL 오류: %.70s (sqlcode=%ld)", + sqlca.sqlerrm.sqlerrmc, sqlca.sqlcode); + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL ROLLBACK WORK; + Fchg(fb, ACQ_RC, 0, (char *)"E900", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; +} diff --git a/legacy-oracle/proc/AUTH_APPROVE.pc b/legacy-oracle/proc/AUTH_APPROVE.pc new file mode 100644 index 0000000..918b06d --- /dev/null +++ b/legacy-oracle/proc/AUTH_APPROVE.pc @@ -0,0 +1,234 @@ +/*==========================================================================*/ +/* AUTH_APPROVE.pc - 승인확정 온라인 서비스 (As-Is 원본, Oracle Pro*C) */ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / Tuxedo ATMI 서비스 AUTH_APPROVE */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* 기능 : 승인 확정(pending->approved) — 원장 상태전이 + 카드 한도차감 */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/au/svc/AUTH_APPROVE.pgc (ECPG/PostgreSQL). 컴파일 제외. */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 1999.05.12 KIM K.H 최초작성 (승인원장 확정전이) */ +/* 2003.10.07 YOON D.H 블랙리스트/velocity 룰 추가, DECODE 등급분기 */ +/* 2010.08.24 LEE S.W 당일 상태분포 재집계, au_summary 연계 */ +/* 2016.02.19 JANG M.K 이상거래 점수(au_fraud_log) 반영 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, CHAR->VARCHAR2 정비 */ +/*==========================================================================*/ +#include +#include +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +#include "aufld.h" + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_card[24]; + VARCHAR h_merch[64]; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_status[2]; + VARCHAR h_cstat[16]; + long h_auth; + long h_amount; + long h_delta; + long h_cnt; + long h_sum; + long h_appr; + long h_black; + long h_vel; + long h_risk; + long h_grade; + long h_mdr_bps; + long h_fee; + long h_vat; + long h_dlim; + long h_avail; + long h_led; +EXEC SQL END DECLARE SECTION; + +/*--------------------------------------------------------------------------*/ +/* AUTH_APPROVE - Tuxedo 서비스 엔트리 */ +/*--------------------------------------------------------------------------*/ +void AUTH_APPROVE(TPSVCINFO *rqst) +{ + FBFR32 *fb = (FBFR32 *)rqst->data; + char card[24], merch[64]; + long rc = 0; + TPINIT *tpi = (TPINIT *)NULL; + + userlog("AUTH_APPROVE 진입: 승인 확정(pending->approved)"); + EXEC SQL WHENEVER SQLERROR GOTO sql_error; + + /* ---- 1) 요청 FML 파싱 -------------------------------------------- */ + h_auth = Fvall(fb, AU_AUTH_ID, 0); + if (h_auth <= 0) { + userlog("AUTH_APPROVE: 식별자 범위오류 auth_id=%ld", h_auth); + Fchg(fb, AU_RC, 0, (char *)"E801", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + h_delta = Fvall(fb, AU_DELTA, 0); + if (h_delta < 0) h_delta = -h_delta; + + /* ---- 2) 대상 승인건 조회 (현재상태/금액/카드/영업일) -------------- */ + /* 무마스터 승인 허용: 카드마스터는 외부조인 (+) 으로 등급만 취득 */ + EXEC SQL + SELECT a.status, + NVL(a.amount, 0), + NVL(a.card_no, ' '), + TO_CHAR(NVL(a.biz_date, SYSDATE),'YYYYMMDD'), + DECODE(NVL(c.risk_grade,'B'), 'A',1, 'B',2, 'C',3, 9) + INTO :h_status, :h_amount, :h_card, :h_bizdate, :h_grade + FROM au_authorization a, card_master c + WHERE a.auth_id = :h_auth + AND c.card_no (+) = a.card_no; + + if (sqlca.sqlcode == 1403) { + userlog("AUTH_APPROVE: 대상 승인건 없음 auth_id=%ld", h_auth); + Fchg(fb, AU_RC, 0, (char *)"E831", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + strcpy(card, (char *)h_card.arr); + + /* ---- 3) 전이 가능여부 분기 (허용 상태에서만 A 로 전이) ------------ */ + if (strncmp((char *)h_status.arr, "A", 1) != 0 && + strncmp((char *)h_status.arr, "P", 1) != 0) { + userlog("AUTH_APPROVE: 상태전이 불가 cur=%.1s auth_id=%ld", + h_status.arr, h_auth); + Fchg(fb, AU_RC, 0, (char *)"E833", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 4) 본처리: 원장 상태전이 (+금액조정 델타) ------------------- */ + EXEC SQL + UPDATE au_authorization + SET status = 'A', updated_at = SYSDATE + WHERE auth_id = :h_auth + AND status IN ('A','P'); + if (sqlca.sqlerrd[2] == 0) { + userlog("AUTH_APPROVE: 반영건수 0 (경합/이미전이) auth_id=%ld", h_auth); + Fchg(fb, AU_RC, 0, (char *)"E835", 0L); + EXEC SQL ROLLBACK WORK; + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + if (h_delta > 0) { + EXEC SQL + UPDATE au_authorization + SET amount = amount + :h_delta, updated_at = SYSDATE + WHERE auth_id = :h_auth; + h_amount += h_delta; + } + + /* ---- 5) 카드 당일 상태분포 재집계 (한도판정 기초) --------------- */ + EXEC SQL + SELECT COUNT(*), NVL(SUM(amount),0), + NVL(SUM(DECODE(status,'A',1,0)),0) + INTO :h_cnt, :h_sum, :h_appr + FROM au_authorization + WHERE card_no = :h_card + AND biz_date = TO_DATE(:h_bizdate,'YYYYMMDD'); + + /* ---- 6) 업무규칙: 블랙리스트/카드상태/velocity/부정점수 --------- */ + EXEC SQL + SELECT COUNT(*) INTO :h_black + FROM au_blacklist + WHERE card_no = :h_card; + if (h_black > 0) { rc = 91; userlog("AUTH_APPROVE: 블랙리스트 적중 card=%s", card); } + + EXEC SQL + SELECT NVL(MAX(status),'NONE'), NVL(MAX(avail_limit),0) + INTO :h_cstat, :h_avail + FROM card_limit + WHERE card_no = :h_card; + if (strncmp((char *)h_cstat.arr, "ACTIVE", 6) != 0 && + strncmp((char *)h_cstat.arr, "NONE", 4) != 0) { + if (rc == 0) rc = 92; + userlog("AUTH_APPROVE: 카드상태 비정상 card=%s status=%.8s", card, h_cstat.arr); + } + + EXEC SQL + SELECT COUNT(*) INTO :h_vel + FROM au_authorization + WHERE card_no = :h_card + AND created_at > SYSDATE - 1/24; + if (h_vel >= 10) { if (rc == 0) rc = 93; + userlog("AUTH_APPROVE: velocity 경보 card=%s 1시간 %ld건", card, h_vel); } + + EXEC SQL + SELECT NVL(MAX(score),0) INTO :h_risk + FROM au_fraud_log + WHERE card_no = :h_card + AND ROWNUM <= 100; + if (h_risk >= 90) userlog("AUTH_APPROVE: 고위험 카드 card=%s 최고점=%ld", card, h_risk); + + /* ---- 7) 상태전이 이력 적재 (감사원장, 시퀀스 .NEXTVAL 채번) ----- */ + EXEC SQL SELECT au_audit_seq.NEXTVAL INTO :h_led FROM DUAL; + EXEC SQL + INSERT INTO au_audit + (audit_id, auth_id, card_no, from_status, to_status, + amount, audit_rc, created_at) + VALUES (:h_led, :h_auth, :h_card, :h_status, 'A', + :h_amount, :rc, SYSDATE); + + /* ---- 8) 가맹점 당일 승인 집계 (au_summary upsert, PL/SQL) ------ */ + Fget(fb, AU_MERCHANT, 0, merch, (FLDLEN32 *)NULL); + strcpy((char *)h_merch.arr, merch); h_merch.len = strlen(merch); + if (h_merch.len > 0) { + EXEC SQL + SELECT COUNT(*), + NVL(SUM(DECODE(status,'A',1,0)),0), + NVL(SUM(amount),0) + INTO :h_cnt, :h_appr, :h_sum + FROM au_authorization + WHERE merchant_id = :h_merch + AND biz_date = TO_DATE(:h_bizdate,'YYYYMMDD'); + EXEC SQL EXECUTE + BEGIN + pkg_settle.upsert_auth_summary(:h_merch, :h_bizdate, + :h_cnt, :h_appr, :h_sum); + END; + END-EXEC; + } + + /* ---- 9) 정산 참고치: MDR 수수료/부가세 (DECODE 요율) ------------ */ + EXEC SQL + SELECT NVL(m.mdr_bps, 25) INTO :h_mdr_bps + FROM merchant m + WHERE m.merchant_id = :h_merch; + h_fee = ROUND(h_amount * h_mdr_bps / 10000); + h_vat = ROUND(h_fee / 10); + + /* ---- 10) 업무체인: 승인확정 한도차감 (LIMIT_DEC PL/SQL 패키지) -- */ + EXEC SQL EXECUTE + BEGIN + pkg_settle.limit_dec(:h_card, :h_amount, :h_avail); + END; + END-EXEC; + + EXEC SQL COMMIT WORK; + + Fchg(fb, AU_STATUS, 0, (char *)"A", 0L); + Fchg(fb, AU_FEE, 0, (char *)&h_fee, 0L); + Fchg(fb, AU_AVAIL, 0, (char *)&h_avail, 0L); + Fchg(fb, AU_RC, 0, (char *)&rc, 0L); + userlog("AUTH_APPROVE 완료: auth_id=%ld amt=%ld fee=%ld rc=%ld", + h_auth, h_amount, h_fee, rc); + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + +sql_error: + userlog("AUTH_APPROVE SQL 오류: %.70s (sqlcode=%ld)", + sqlca.sqlerrm.sqlerrmc, sqlca.sqlcode); + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL ROLLBACK WORK; + Fchg(fb, AU_RC, 0, (char *)"E900", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; +} diff --git a/legacy-oracle/proc/CL_DAILY.pc b/legacy-oracle/proc/CL_DAILY.pc new file mode 100644 index 0000000..2eeb3dd --- /dev/null +++ b/legacy-oracle/proc/CL_DAILY.pc @@ -0,0 +1,230 @@ +/*==========================================================================*/ +/* CL_DAILY.pc - 일마감 배치 (As-Is 원본, Oracle Pro*C) */ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / 일일 마감 배치 CLDAILY (야간 JOB) */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* 실행 : cldaily (인자 생략시 전영업일 = SYSDATE-1) */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/cl/svc/CL_DAILY.pgc (ECPG/PostgreSQL, Tuxedo 서비스화). */ +/* 원본은 순수 배치(main+커서루프) 였다. 컴파일 대상 아님(빌드 제외). */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 1999.02.15 KIM K.H 최초작성 (가맹점별 당일 매입 집계/마감이력) */ +/* 2003.10.07 PARK J.S 기간잠금(cl_period_lock) 선점검 추가 */ +/* 2009.05.22 LEE S.W 보류(H)/미매칭 잔존시 경고후 마감강행 정책 반영 */ +/* 2015.08.11 JUNG M.K 마감 규모별 심각도(sev) 분류, cm_log 요약적재 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, CHAR->VARCHAR2 정비 */ +/*==========================================================================*/ +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_user[32]; + VARCHAR h_pass[32]; + VARCHAR h_dbstr[32]; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_merch[16]; + VARCHAR h_mname[64]; + VARCHAR h_status[2]; + VARCHAR h_zmsg[64]; + long h_locked; + long h_pend; + long h_unmatch; + long h_prevcnt; + long h_cid; + long h_txncnt; + long h_gross; + long h_fee; + long h_net; + long h_totcnt; + long h_totamt; + long h_nmerch; + long h_zsev; +EXEC SQL END DECLARE SECTION; + +/* 가맹점별 당일 확정매입 커서 (취소 C 제외) — 마감 대상 스윕 */ +EXEC SQL DECLARE cur_merch CURSOR FOR + SELECT m.merchant_id, + NVL(m.name,'(무명)'), + NVL(m.status,'N'), + COUNT(p.purchase_id), + NVL(SUM(p.amount), 0), + NVL(SUM(p.fee), 0), + NVL(SUM(p.net), 0) + FROM merchant m, purchase p + WHERE p.merchant_id (+) = m.merchant_id + AND p.biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND NVL(p.status,'A') <> 'C' + GROUP BY m.merchant_id, m.name, m.status + HAVING COUNT(p.purchase_id) > 0 + ORDER BY m.merchant_id; + +static void sql_error(const char *stage); + +int main(int argc, char **argv) +{ + int done = 0; + + /* ---- 0) 접속 (배치는 자기 세션으로 CONNECT) ---------------------- */ + strcpy((char *)h_user.arr, "clbatch"); h_user.len = strlen((char *)h_user.arr); + strcpy((char *)h_pass.arr, "clbatch"); h_pass.len = strlen((char *)h_pass.arr); + strcpy((char *)h_dbstr.arr, "ACQPRD"); h_dbstr.len = strlen((char *)h_dbstr.arr); + + EXEC SQL WHENEVER SQLERROR GOTO connect_err; + EXEC SQL CONNECT :h_user IDENTIFIED BY :h_pass USING :h_dbstr; + + /* 이후 오류는 표준 라벨분기 */ + EXEC SQL WHENEVER SQLERROR GOTO sqlfail; + + /* ---- 1) 영업일 결정 (인자 없으면 전영업일 SYSDATE-1) ------------- */ + if (argc >= 2) { + strncpy((char *)h_bizdate.arr, argv[1], 8); + h_bizdate.arr[8] = '\0'; + h_bizdate.len = strlen((char *)h_bizdate.arr); + } else { + EXEC SQL SELECT TO_CHAR(SYSDATE-1,'YYYYMMDD') INTO :h_bizdate FROM DUAL; + } + printf("[CL_DAILY] 일마감 개시 bizdate=%.8s\n", h_bizdate.arr); + + /* ---- 2) 기간 잠금 선점검 (잠기면 즉시 중단) --------------------- */ + EXEC SQL SELECT COUNT(*) INTO :h_locked + FROM cl_period_lock + WHERE period_key = :h_bizdate + AND locked = 'Y'; + if (h_locked > 0) { + fprintf(stderr, "[CL_DAILY] 기간잠금 상태 - 일마감 불가 bizdate=%.8s\n", + h_bizdate.arr); + EXEC SQL ROLLBACK WORK RELEASE; + return 4; + } + + /* ---- 3) 사전상태 점검 (보류/미매칭/기마감) — 경고후 강행 -------- */ + EXEC SQL SELECT COUNT(*) INTO :h_pend + FROM purchase + WHERE biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND status = 'H'; + EXEC SQL SELECT NVL(COUNT(*),0) INTO :h_unmatch + FROM ac_unmatch + WHERE biz_date = TO_DATE(:h_bizdate,'YYYYMMDD'); + EXEC SQL SELECT COUNT(*) INTO :h_prevcnt + FROM cl_close_log + WHERE close_type = 'DAILY' + AND period_key = :h_bizdate + AND status <> 'X'; + if (h_pend > 0) + printf("[CL_DAILY] 경고: 보류(H) %ld건 잔존 - 마감 진행\n", h_pend); + if (h_unmatch > 0) + printf("[CL_DAILY] 경고: 미매칭 %ld건 잔존 - 마감 진행\n", h_unmatch); + if (h_prevcnt > 0) + printf("[CL_DAILY] 기마감 %ld건 존재 - 신규 마감이력 추가생성\n", h_prevcnt); + + /* ---- 4) 마감헤더 채번 (시퀀스 .NEXTVAL) ------------------------ */ + EXEC SQL SELECT cl_close_seq.NEXTVAL INTO :h_cid FROM DUAL; + EXEC SQL + INSERT INTO cl_close_log + (close_id, close_type, period_key, status, + txn_count, total_amount, opened_at) + VALUES (:h_cid, 'DAILY', :h_bizdate, 'O', + 0, 0, SYSDATE); + + /* ---- 5) 본처리: 가맹점별 집계 커서 루프 ------------------------ */ + h_totcnt = 0; h_totamt = 0; h_nmerch = 0; + EXEC SQL OPEN cur_merch; + for (;;) { + EXEC SQL WHENEVER NOTFOUND DO done = 1; + EXEC SQL FETCH cur_merch + INTO :h_merch, :h_mname, :h_status, + :h_txncnt, :h_gross, :h_fee, :h_net; + if (done) break; + + /* 비활성 가맹점은 집계만 하고 정산연계는 보류(로그) */ + if (strncmp((char *)h_status.arr, "A", 1) != 0) + printf("[CL_DAILY] 주의: 비활성(%.1s) 가맹점 %.*s 마감집계 포함\n", + h_status.arr, h_merch.len, h_merch.arr); + + EXEC SQL + INSERT INTO cl_close_detail + (close_id, merchant_id, txn_count, + gross_amount, fee_amount, net_amount, created_at) + VALUES (:h_cid, :h_merch, :h_txncnt, + :h_gross, :h_fee, :h_net, SYSDATE); + + /* 마감된 매입행을 'S'(마감완료)로 상태전이 */ + EXEC SQL + UPDATE purchase + SET status = DECODE(status, 'A', 'S', status), + closed_at = SYSDATE, + close_id = :h_cid + WHERE merchant_id = :h_merch + AND biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND status = 'A'; + + h_totcnt += h_txncnt; + h_totamt += h_gross; + h_nmerch += 1; + if (h_nmerch % 500 == 0) { + printf("[CL_DAILY] 진행 %ld개 가맹점 누적 %ld건 %ld원 - 중간커밋\n", + h_nmerch, h_totcnt, h_totamt); + EXEC SQL COMMIT WORK; + } + } + EXEC SQL CLOSE cur_merch; + + /* ---- 6) 마감헤더 확정 (집계 반영, 상태 O->C) ------------------- */ + EXEC SQL + UPDATE cl_close_log + SET txn_count = :h_totcnt, + total_amount = :h_totamt, + status = 'C', + closed_at = SYSDATE + WHERE close_id = :h_cid; + + /* ---- 7) 마감 규모별 심각도 분류 (DECODE 대체 if-ladder) -------- */ + if (h_totcnt == 0) { h_zsev = 0; strcpy((char *)h_zmsg.arr, "마감대상 없음"); } + else if (h_totamt >= 500000000L){ h_zsev = 3; strcpy((char *)h_zmsg.arr, "고액 마감구간"); } + else if (h_totamt >= 50000000L) { h_zsev = 2; strcpy((char *)h_zmsg.arr, "중액 마감구간"); } + else { h_zsev = 1; strcpy((char *)h_zmsg.arr, "소액 마감구간"); } + h_zmsg.len = strlen((char *)h_zmsg.arr); + + EXEC SQL + INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (cm_log_seq.NEXTVAL, 'CL_DAILY', + DECODE(SIGN(:h_zsev - 2), 1, 'W', 'I'), + 'CL_DAILY 일마감 sev='||:h_zsev||' '||:h_zmsg + ||' cnt='||:h_totcnt||' amt='||:h_totamt, + TO_DATE(:h_bizdate,'YYYYMMDD')); + + /* ---- 8) 최종 커밋 --------------------------------------------- */ + EXEC SQL COMMIT WORK; + + printf("[CL_DAILY] 일마감 완료 cid=%ld 가맹점=%ld 건수=%ld 금액=%ld sev=%ld(%s)\n", + h_cid, h_nmerch, h_totcnt, h_totamt, h_zsev, h_zmsg.arr); + EXEC SQL COMMIT WORK RELEASE; + return 0; + +connect_err: + fprintf(stderr, "[CL_DAILY] DB 접속 실패: %.70s\n", sqlca.sqlerrm.sqlerrmc); + return 8; + +sqlfail: + sql_error("일마감 본처리"); + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL ROLLBACK WORK RELEASE; + return 12; +} + +/*--------------------------------------------------------------------------*/ +/* sql_error - 오류 로깅 (sqlca/oraca) */ +/*--------------------------------------------------------------------------*/ +static void sql_error(const char *stage) +{ + fprintf(stderr, "[CL_DAILY] SQL 오류 (%s): %.70s\n", + stage, sqlca.sqlerrm.sqlerrmc); + fprintf(stderr, " sqlcode=%ld rows=%ld stmt=%.60s\n", + sqlca.sqlcode, sqlca.sqlerrd[2], oraca.orastxt.orastxtc); +} diff --git a/legacy-oracle/proc/PAY_FILEGEN.pc b/legacy-oracle/proc/PAY_FILEGEN.pc new file mode 100644 index 0000000..65c86fa --- /dev/null +++ b/legacy-oracle/proc/PAY_FILEGEN.pc @@ -0,0 +1,247 @@ +/*==========================================================================*/ +/* PAY_FILEGEN.pc - 지급파일 생성 배치 (As-Is 원본, Oracle Pro*C) */ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / 지급(펌뱅킹) 파일 생성 배치 PAYFGEN */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* 실행 : payfgen (인자 생략시 SYSDATE) */ +/* 기능 : 당일 승인(A) 지급건을 커서로 스윕하여 py_file 헤더 채번 + */ +/* 평문 지급레코드를 UTL_FILE 로 디스크 파일에 기록(펌뱅킹 규격). */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/py/svc/PAY_FILEGEN.pgc (ECPG/PostgreSQL, Tuxedo 서비스화; */ +/* 디스크 I/O 는 별도 xfer 로 분리됨). 컴파일 대상 아님(빌드 제외). */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 2000.07.03 KIM K.H 최초작성 (전은행 공통 지급파일 EBCDIC->ASCII) */ +/* 2006.12.20 PARK J.S 정책 파라미터(cm_param) 기반 최대건수 분할 */ +/* 2013.04.09 LEE S.W UTL_FILE 로 헤더/명세/트레일러 3레코드 규격화 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, CHAR->VARCHAR2 정비 */ +/*==========================================================================*/ +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_user[32]; + VARCHAR h_pass[32]; + VARCHAR h_dbstr[32]; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_fname[64]; + VARCHAR h_dir[32]; + VARCHAR h_bankcd[4]; + VARCHAR h_acctno[20]; + VARCHAR h_merch[16]; + VARCHAR h_line[256]; + VARCHAR h_pval[16]; + long h_fileid; + long h_payid; + long h_amount; + long h_fee; + long h_net; + long h_reccnt; + long h_totamt; + long h_thr; + long h_seq; +EXEC SQL END DECLARE SECTION; + +/* 당일 승인(A) 지급 대상 커서 — 계좌 마스터 외부조인으로 은행/계좌 획득 */ +EXEC SQL DECLARE cur_pay CURSOR FOR + SELECT p.payout_id, + p.merchant_id, + NVL(a.bank_code, '000'), + NVL(a.account_no, 'X'), + NVL(p.amount, 0), + NVL(p.fee, 0), + NVL(p.net, 0) + FROM py_payout p, py_account a + WHERE a.merchant_id (+) = p.merchant_id + AND p.biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND p.status = 'A' + ORDER BY p.merchant_id, p.payout_id; + +static void sql_error(const char *stage); + +int main(int argc, char **argv) +{ + int done = 0; + + strcpy((char *)h_user.arr, "pybatch"); h_user.len = strlen((char *)h_user.arr); + strcpy((char *)h_pass.arr, "pybatch"); h_pass.len = strlen((char *)h_pass.arr); + strcpy((char *)h_dbstr.arr, "ACQPRD"); h_dbstr.len = strlen((char *)h_dbstr.arr); + + EXEC SQL WHENEVER SQLERROR GOTO connect_err; + EXEC SQL CONNECT :h_user IDENTIFIED BY :h_pass USING :h_dbstr; + EXEC SQL WHENEVER SQLERROR GOTO sqlfail; + + /* ---- 1) 영업일 결정 -------------------------------------------- */ + if (argc >= 2) { + strncpy((char *)h_bizdate.arr, argv[1], 8); + h_bizdate.arr[8] = '\0'; + h_bizdate.len = strlen((char *)h_bizdate.arr); + } else { + EXEC SQL SELECT TO_CHAR(SYSDATE,'YYYYMMDD') INTO :h_bizdate FROM DUAL; + } + printf("[PAY_FILEGEN] 지급파일 생성 개시 bizdate=%.8s\n", h_bizdate.arr); + + /* ---- 2) 편성 대상 사전집계 (건수/총액) ------------------------- */ + EXEC SQL + SELECT COUNT(*), NVL(SUM(net), 0) + INTO :h_reccnt, :h_totamt + FROM py_payout + WHERE biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND status = 'A'; + if (h_reccnt == 0) { + printf("[PAY_FILEGEN] 편성 대상 승인건 없음 - 파일생성 생략\n"); + EXEC SQL ROLLBACK WORK RELEASE; + return 0; + } + if (h_totamt <= 0) { + fprintf(stderr, "[PAY_FILEGEN] 편성 총액 비정상 amt=%ld\n", h_totamt); + EXEC SQL ROLLBACK WORK RELEASE; + return 4; + } + + /* ---- 3) 정책 파라미터 조회 (최대 레코드수) — NVL 널가드 --------- */ + EXEC SQL + SELECT NVL(MAX(param_val), '100000') + INTO :h_pval + FROM cm_param + WHERE param_key = 'pay_filegen_max_rec'; + h_thr = atol((char *)h_pval.arr); + if (h_thr <= 0) h_thr = 100000; + if (h_reccnt > h_thr) { + fprintf(stderr, "[PAY_FILEGEN] 최대 레코드수 초과 %ld > %ld - 분할 필요\n", + h_reccnt, h_thr); + EXEC SQL ROLLBACK WORK RELEASE; + return 5; + } + + /* ---- 4) 파일 채번 및 헤더행 적재 (시퀀스 .NEXTVAL) ------------- */ + EXEC SQL SELECT py_file_seq.NEXTVAL INTO :h_fileid FROM DUAL; + sprintf((char *)h_fname.arr, "PAYOUT_%.8s_%ld.dat", + h_bizdate.arr, h_fileid); + h_fname.len = strlen((char *)h_fname.arr); + strcpy((char *)h_dir.arr, "PAYOUT_OUT"); /* DIRECTORY 객체명 */ + h_dir.len = strlen((char *)h_dir.arr); + + EXEC SQL + INSERT INTO py_file + (file_id, biz_date, file_name, record_count, total_amount, status, created_at) + VALUES (:h_fileid, TO_DATE(:h_bizdate,'YYYYMMDD'), :h_fname, + :h_reccnt, :h_totamt, 'G', SYSDATE); + + /* UTL_FILE 로 파일 오픈 후 헤더레코드('H') 기록 (PL/SQL 익명블록) */ + EXEC SQL EXECUTE + BEGIN + pkg_payfile.open_file(:h_dir, :h_fname); + pkg_payfile.put_header(:h_bizdate, :h_reccnt, :h_totamt); + END; + END-EXEC; + + /* ---- 5) 본처리: 지급건 커서 루프 + 명세레코드('D') 기록 -------- */ + h_seq = 0; + EXEC SQL OPEN cur_pay; + for (;;) { + EXEC SQL WHENEVER NOTFOUND DO done = 1; + EXEC SQL FETCH cur_pay + INTO :h_payid, :h_merch, :h_bankcd, :h_acctno, + :h_amount, :h_fee, :h_net; + if (done) break; + + h_seq += 1; + + /* 무계좌(외부조인 미스) 건은 파일에서 제외하고 보류 전이 */ + if (strncmp((char *)h_bankcd.arr, "000", 3) == 0) { + printf("[PAY_FILEGEN] 계좌미등록 payout=%ld merch=%.*s - 보류전이\n", + h_payid, h_merch.len, h_merch.arr); + EXEC SQL + UPDATE py_payout + SET status = 'H', updated_at = SYSDATE + WHERE payout_id = :h_payid; + h_seq -= 1; + continue; + } + + /* 펌뱅킹 명세레코드 조립 (고정폭: 구분1 은행3 계좌20 금액13) */ + sprintf((char *)h_line.arr, "D%3.3s%-20.20s%013ld", + h_bankcd.arr, h_acctno.arr, h_net); + h_line.len = strlen((char *)h_line.arr); + + EXEC SQL EXECUTE + BEGIN + pkg_payfile.put_line(:h_line); + END; + END-EXEC; + + /* 지급건을 이 파일로 편성 ('A'->'F') */ + EXEC SQL + UPDATE py_payout + SET file_id = :h_fileid, + status = DECODE(status, 'A', 'F', status), + updated_at = SYSDATE + WHERE payout_id = :h_payid; + + if (h_seq % 1000 == 0) { + printf("[PAY_FILEGEN] 진행 %ld건 편성 - 중간커밋\n", h_seq); + EXEC SQL COMMIT WORK; + } + } + EXEC SQL CLOSE cur_pay; + + /* ---- 6) 트레일러('T') 기록 및 파일 클로즈 --------------------- */ + EXEC SQL EXECUTE + BEGIN + pkg_payfile.put_trailer(:h_seq, :h_totamt); + pkg_payfile.close_file; + END; + END-EXEC; + + /* ---- 7) 파일헤더 확정 (실제 기록건수 반영, 'G'->'C') ----------- */ + EXEC SQL + UPDATE py_file + SET record_count = :h_seq, + status = 'C', + closed_at = SYSDATE + WHERE file_id = :h_fileid; + + EXEC SQL + INSERT INTO cm_log (log_id, svc_name, log_level, msg, biz_date) + VALUES (cm_log_seq.NEXTVAL, 'PAY_FILEGEN', 'I', + 'PAY_FILEGEN file='||:h_fileid||' name='||:h_fname + ||' rec='||:h_seq||' amt='||:h_totamt, + TO_DATE(:h_bizdate,'YYYYMMDD')); + + /* ---- 8) 최종 커밋 --------------------------------------------- */ + EXEC SQL COMMIT WORK; + + printf("[PAY_FILEGEN] 완료 file=%ld name=%.*s rec=%ld amt=%ld\n", + h_fileid, h_fname.len, h_fname.arr, h_seq, h_totamt); + EXEC SQL COMMIT WORK RELEASE; + return 0; + +connect_err: + fprintf(stderr, "[PAY_FILEGEN] DB 접속 실패: %.70s\n", sqlca.sqlerrm.sqlerrmc); + return 8; + +sqlfail: + sql_error("지급파일 생성"); + /* UTL_FILE 핸들 정리 (best-effort) */ + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL EXECUTE BEGIN pkg_payfile.close_file; END; END-EXEC; + EXEC SQL ROLLBACK WORK RELEASE; + return 12; +} + +/*--------------------------------------------------------------------------*/ +/* sql_error - 오류 로깅 (sqlca/oraca) */ +/*--------------------------------------------------------------------------*/ +static void sql_error(const char *stage) +{ + fprintf(stderr, "[PAY_FILEGEN] SQL 오류 (%s): %.70s\n", + stage, sqlca.sqlerrm.sqlerrmc); + fprintf(stderr, " sqlcode=%ld rows=%ld\n", + sqlca.sqlcode, sqlca.sqlerrd[2]); +} diff --git a/legacy-oracle/proc/RECONCILE.pc b/legacy-oracle/proc/RECONCILE.pc new file mode 100644 index 0000000..f10394e --- /dev/null +++ b/legacy-oracle/proc/RECONCILE.pc @@ -0,0 +1,224 @@ +/*==========================================================================*/ +/* RECONCILE.pc - 대사(對査) 온라인 서비스 (As-Is 원본, Oracle Pro*C) */ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / Tuxedo ATMI 서비스 RECONCILE */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* 기능 : 당일 매입 vs 원장 대사 — 커서순회 매칭분류 + 예외적재 + 정산연계. */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/rc/svc/RECONCILE.pgc (ECPG/PostgreSQL). 컴파일 제외. */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 2001.07.16 SEO K.T 최초작성 (일 매입-원장 대사) */ +/* 2005.12.02 HAN Y.J 허용오차(tolerance) 매칭 도입, DECODE 등급 */ +/* 2009.04.27 LEE S.W 조직/코드 트리 CONNECT BY 집계 추가 */ +/* 2015.10.13 JANG M.K 예외원장(rc_exception) 분리, 감사로그 표준화 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, 커서/시퀀스 정비 */ +/*==========================================================================*/ +#include +#include +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +#include "rcfld.h" + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_merch[64]; + VARCHAR h_status[16]; + VARCHAR h_pstat[2]; + VARCHAR h_orgcode[16]; /* 조직/코드 트리 루트 */ + VARCHAR h_orgname[64]; + long h_pid; + long h_amt; + long h_fee; + long h_net; + long h_act; + long h_exp; + long h_gross; + long h_cnt; + long h_mcnt; + long h_ecnt; + long h_mdr_bps; + long h_limit; + long h_led; + long h_lvl; /* 코드트리 계층레벨 */ + long h_orgsum; +EXEC SQL END DECLARE SECTION; + +/*--------------------------------------------------------------------------*/ +/* RECONCILE - Tuxedo 서비스 엔트리 */ +/*--------------------------------------------------------------------------*/ +void RECONCILE(TPSVCINFO *rqst) +{ + FBFR32 *fb = (FBFR32 *)rqst->data; + char bizdate[9], merch[64]; + long tol = 0, rc_code = 0, adiff = 0; + long scan_rows = 0, matched = 0, unmatched = 0, unmatched_amt = 0; + + userlog("RECONCILE 진입: 당일 매입-원장 대사"); + EXEC SQL WHENEVER SQLERROR GOTO sql_error; + + /* ---- 1) 입력 검증 ------------------------------------------------ */ + Fget(fb, RC_BIZDATE, 0, bizdate, (FLDLEN32 *)NULL); + if (strlen(bizdate) < 8) { + EXEC SQL SELECT TO_CHAR(SYSDATE,'YYYYMMDD') INTO :h_bizdate FROM DUAL; + strcpy(bizdate, (char *)h_bizdate.arr); + } + strcpy((char *)h_bizdate.arr, bizdate); h_bizdate.len = strlen(bizdate); + h_pid = Fvall(fb, RC_PURCHASE_ID, 0); + h_net = Fvall(fb, RC_NET, 0); + + /* ---- 2) 마스터/정책 조회 (미커밋 매입은 전문값, 외부조인 (+)) ---- */ + EXEC SQL + SELECT NVL(p.amount,0), NVL(p.fee,0), NVL(p.net,:h_net), + NVL(p.status,'A'), NVL(m.merchant_id, ' '), + NVL(m.mdr_bps, 25), NVL(m.daily_limit, 100000000) + INTO :h_amt, :h_fee, :h_net, :h_pstat, :h_merch, :h_mdr_bps, :h_limit + FROM purchase p, merchant m + WHERE p.purchase_id = :h_pid + AND m.merchant_id (+) = p.merchant_id; + if (sqlca.sqlcode == 1403) { + h_amt = Fvall(fb, RC_AMOUNT, 0); + h_fee = Fvall(fb, RC_FEE, 0); + h_net = Fvall(fb, RC_NET, 0); + Fget(fb, RC_MERCHANT, 0, merch, (FLDLEN32 *)NULL); + strcpy((char *)h_merch.arr, merch); h_merch.len = strlen(merch); + h_mdr_bps = 25; h_limit = 100000000; + userlog("RECONCILE: 체인 신규매입(미커밋) pid=%ld — 전문값 대사", h_pid); + } + + /* ---- 3) 당일 매입/대사로그/예외 집계 (NVL 널가드) --------------- */ + EXEC SQL + SELECT COUNT(*), NVL(SUM(amount),0) + INTO :h_cnt, :h_gross + FROM purchase + WHERE biz_date = TO_DATE(:h_bizdate,'YYYYMMDD'); + EXEC SQL + SELECT COUNT(*) INTO :h_mcnt + FROM rc_match_log + WHERE biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND status = 'MATCHED'; + EXEC SQL + SELECT COUNT(*) INTO :h_ecnt + FROM rc_exception + WHERE biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND resolved = 'N'; + + /* ---- 4) 거래집합 커서순회 + 허용오차 매칭분류 ------------------- */ + EXEC SQL DECLARE cur_reconcile CURSOR FOR + SELECT p.purchase_id, NVL(p.net,0) + FROM purchase p + WHERE p.biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + ORDER BY p.purchase_id; + EXEC SQL OPEN cur_reconcile; + for (;;) { + EXEC SQL WHENEVER NOTFOUND DO break; + EXEC SQL FETCH cur_reconcile INTO :h_pid, :h_exp; + EXEC SQL + SELECT NVL(SUM(amount),0) INTO :h_act + FROM ledger + WHERE purchase_id = :h_pid + AND entry_type IN ('RECON','SETTLE'); + adiff = h_exp - h_act; + if (adiff < 0) adiff = -adiff; + scan_rows++; + if (adiff <= (tol > 0 ? tol : 0)) matched++; + else { unmatched++; unmatched_amt += adiff; } + } + EXEC SQL WHENEVER NOTFOUND CONTINUE; + EXEC SQL CLOSE cur_reconcile; + userlog("RECONCILE 대사스캔: scan=%ld match=%ld unmatch=%ld", + scan_rows, matched, unmatched); + + /* ---- 5) 조직/코드 트리 집계 (계층질의 CONNECT BY PRIOR) --------- */ + /* 가맹점이 속한 정산조직 코드트리를 루트→하위로 전개하여 소속 조직의 */ + /* 당일 정산 순액 합계를 취득한다(대사 대상 조직 범위 확정용). */ + EXEC SQL + SELECT NVL(org_code,'ROOT'), NVL(org_name,'-') + INTO :h_orgcode, :h_orgname + FROM rc_org_map + WHERE merchant_id = :h_merch + AND ROWNUM = 1; + if (sqlca.sqlcode == 1403) { strcpy((char *)h_orgcode.arr, "ROOT"); + h_orgcode.len = 4; } + EXEC SQL + SELECT NVL(SUM(s.net),0), NVL(MAX(LEVEL),1) + INTO :h_orgsum, :h_lvl + FROM settlement s, rc_org o + WHERE s.biz_date = TO_DATE(:h_bizdate,'YYYYMMDD') + AND s.org_code = o.org_code + AND o.org_code IN ( + SELECT org_code + FROM rc_org + START WITH org_code = :h_orgcode + CONNECT BY PRIOR org_code = parent_code); + userlog("RECONCILE 코드트리: org=%.8s depth=%ld 정산합=%ld", + h_orgcode.arr, h_lvl, h_orgsum); + + /* ---- 6) 업무규칙 분기 (매칭율 기반 상태판정, DECODE) ------------- */ + if (h_gross <= 0 && scan_rows == 0) { + rc_code = 0; strcpy((char *)h_status.arr, "NODATA"); + } else if (unmatched == 0) { + rc_code = 0; strcpy((char *)h_status.arr, "MATCHED"); + } else if (unmatched * 100 <= (scan_rows > 0 ? scan_rows : 1) * 5) { + rc_code = 1; strcpy((char *)h_status.arr, "TOLERATED"); + } else { + rc_code = 2; strcpy((char *)h_status.arr, "EXCEPTION"); + } + h_status.len = strlen((char *)h_status.arr); + /* 감사 심각도 라벨 (DECODE 코드분기) */ + EXEC SQL + SELECT DECODE(:rc_code, 0,'I', 1,'W', 2,'E', 'X') + INTO :h_pstat + FROM DUAL; + + /* ---- 7) 본처리: RECON 원장 적재 (.NEXTVAL 채번) ---------------- */ + EXEC SQL SELECT ledger_seq.NEXTVAL INTO :h_led FROM DUAL; + EXEC SQL + INSERT INTO ledger + (ledger_id, purchase_id, entry_type, amount, biz_date, posted_at) + VALUES (:h_led, :h_pid, 'RECON', :h_net, + TO_DATE(:h_bizdate,'YYYYMMDD'), SYSDATE); + + /* ---- 8) 불일치 예외 적재 + 감사로그 (PL/SQL 대사 패키지) -------- */ + if (rc_code >= 2) { + EXEC SQL + INSERT INTO rc_exception + (exc_id, biz_date, merchant_id, unmatch_cnt, unmatch_amt, + resolved, created_at) + VALUES (rc_exception_seq.NEXTVAL, TO_DATE(:h_bizdate,'YYYYMMDD'), + :h_merch, :unmatched, :unmatched_amt, 'N', SYSDATE); + } + EXEC SQL EXECUTE + BEGIN + pkg_settle.recon_audit(:h_bizdate, :h_merch, :scan_rows, + :matched, :unmatched, :rc_code); + END; + END-EXEC; + + EXEC SQL COMMIT WORK; + + /* ---- 9) 응답부 ------------------------------------------------- */ + Fchg(fb, RC_STATUS, 0, (char *)h_status.arr, 0L); + Fchg(fb, RC_MATCHED, 0, (char *)&matched, 0L); + Fchg(fb, RC_UNMATCHED, 0, (char *)&unmatched, 0L); + Fchg(fb, RC_RC, 0, (char *)&rc_code, 0L); + userlog("RECONCILE 완료: biz=%.8s status=%.10s rc=%ld orgsum=%ld", + h_bizdate.arr, h_status.arr, rc_code, h_orgsum); + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + +sql_error: + userlog("RECONCILE SQL 오류: %.70s (sqlcode=%ld)", + sqlca.sqlerrm.sqlerrmc, sqlca.sqlcode); + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL ROLLBACK WORK; + Fchg(fb, RC_RC, 0, (char *)"E900", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; +} diff --git a/legacy-oracle/proc/SETTLE.pc b/legacy-oracle/proc/SETTLE.pc new file mode 100644 index 0000000..4ea2fe7 --- /dev/null +++ b/legacy-oracle/proc/SETTLE.pc @@ -0,0 +1,162 @@ +/*==========================================================================*/ +/* SETTLE.pc - 정산확정 온라인 서비스 (As-Is 원본, Oracle Pro*C) */ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / Tuxedo ATMI 서비스 SETTLE */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* 기능 : 매입건 정산 확정 — 정산원장(settlement) 채번·적재 + 원장 기표. */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/st/svc/SETTLE.pgc (ECPG/PostgreSQL). 컴파일 제외. */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 2000.03.21 PARK J.S 최초작성 (정산확정 원장적재) */ +/* 2006.09.14 HAN Y.J 가맹점 지급주기(D+n) DECODE 반영 */ +/* 2012.05.08 LEE S.W 정산 중복확정 방지, ROWNUM 최근건 조회 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, 시퀀스/커밋 표준화 */ +/*==========================================================================*/ +#include +#include +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +#include "stfld.h" + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_merch[64]; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_pdate[9]; + VARCHAR h_pstat[2]; + VARCHAR h_paycyc[4]; /* 지급주기 코드 */ + long h_pid; + long h_sid; + long h_amount; + long h_fee; + long h_net; + long h_dcnt; + long h_dup; + long h_addday; +EXEC SQL END DECLARE SECTION; + +/*--------------------------------------------------------------------------*/ +/* SETTLE - Tuxedo 서비스 엔트리 */ +/*--------------------------------------------------------------------------*/ +void SETTLE(TPSVCINFO *rqst) +{ + FBFR32 *fb = (FBFR32 *)rqst->data; + char merch[64], bizdate[9]; + + userlog("SETTLE 진입: 정산 확정"); + EXEC SQL WHENEVER SQLERROR GOTO sql_error; + + /* ---- 1) 요청 FML 파싱 -------------------------------------------- */ + h_pid = Fvall(fb, ST_PURCHASE_ID, 0); + h_net = Fvall(fb, ST_NET, 0); + Fget(fb, ST_MERCHANT, 0, merch, (FLDLEN32 *)NULL); + Fget(fb, ST_BIZDATE, 0, bizdate, (FLDLEN32 *)NULL); + strcpy((char *)h_merch.arr, merch); h_merch.len = strlen(merch); + strcpy((char *)h_bizdate.arr, bizdate); h_bizdate.len = strlen(bizdate); + if (h_bizdate.len == 0) { + EXEC SQL SELECT TO_CHAR(SYSDATE,'YYYYMMDD') INTO :h_bizdate FROM DUAL; + } + if (h_pid <= 0) { + userlog("SETTLE: 매입 ID 필수 pid=%ld", h_pid); + Fchg(fb, ST_RC, 0, (char *)"E802", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 2) 매입 상태/금액 확인 + 정산액 보정 (NVL/DECODE) ----------- */ + EXEC SQL + SELECT NVL(p.amount,0), NVL(p.fee,0), NVL(p.net,:h_net), + NVL(p.status,'A') + INTO :h_amount, :h_fee, :h_net, :h_pstat + FROM purchase p + WHERE p.purchase_id = :h_pid; + if (sqlca.sqlcode == 1403) { + h_amount = Fvall(fb, ST_AMOUNT, 0); + userlog("SETTLE: 미커밋 매입 pid=%ld — 전문 net=%ld 사용", h_pid, h_net); + } + + /* ---- 3) 중복확정 방지: 동일 매입 확정 정산 존재여부 (ROWNUM) ---- */ + EXEC SQL + SELECT NVL(MAX(settlement_id),0) + INTO :h_dup + FROM settlement + WHERE purchase_id = :h_pid + AND status = 'SETTLED' + AND ROWNUM = 1; + if (h_dup > 0) { + userlog("SETTLE: 이미 확정된 정산 pid=%ld sid=%ld — 스킵", h_pid, h_dup); + Fchg(fb, ST_SETTLE_ID, 0, (char *)&h_dup, 0L); + Fchg(fb, ST_STATUS, 0, (char *)"D", 0L); + EXEC SQL COMMIT WORK; + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 4) 가맹점 지급주기 조회 → 지급예정일 산정 (DECODE D+n) ----- */ + EXEC SQL + SELECT NVL(paycycle,'D1'), + DECODE(NVL(paycycle,'D1'), 'D0',0, 'D1',1, 'D2',2, 'W1',7, 1) + INTO :h_paycyc, :h_addday + FROM merchant + WHERE merchant_id = :h_merch; + EXEC SQL + SELECT TO_CHAR(TO_DATE(:h_bizdate,'YYYYMMDD') + :h_addday,'YYYYMMDD') + INTO :h_pdate + FROM DUAL; + + /* ---- 5) 본처리: 정산원장 채번(.NEXTVAL) 및 적재 ----------------- */ + EXEC SQL SELECT settlement_seq.NEXTVAL INTO :h_sid FROM DUAL; + EXEC SQL + INSERT INTO settlement + (settlement_id, purchase_id, merchant_id, net, + biz_date, pay_date, status, created_at) + VALUES (:h_sid, :h_pid, :h_merch, :h_net, + TO_DATE(:h_bizdate,'YYYYMMDD'), + TO_DATE(:h_pdate,'YYYYMMDD'), 'SETTLED', SYSDATE); + + /* ---- 6) 정산원장 기표 (ledger SETTLE 엔트리) ------------------- */ + EXEC SQL + INSERT INTO ledger + (ledger_id, purchase_id, entry_type, amount, biz_date, posted_at) + VALUES (ledger_seq.NEXTVAL, :h_pid, 'SETTLE', :h_net, + TO_DATE(:h_bizdate,'YYYYMMDD'), SYSDATE); + + /* ---- 7) 매입 상태 정산완료 전이 ------------------------------- */ + EXEC SQL + UPDATE purchase + SET status = 'S', settled_at = SYSDATE + WHERE purchase_id = :h_pid + AND status <> 'C'; + + /* ---- 8) 정산확정-수수료산출 연계 (PL/SQL 정산 패키지) ---------- */ + EXEC SQL EXECUTE + BEGIN + pkg_settle.settle_confirm(:h_sid, :h_pid, :h_merch, :h_net); + END; + END-EXEC; + + EXEC SQL COMMIT WORK; + + Fchg(fb, ST_SETTLE_ID, 0, (char *)&h_sid, 0L); + Fchg(fb, ST_NET, 0, (char *)&h_net, 0L); + Fchg(fb, ST_STATUS, 0, (char *)"S", 0L); + userlog("SETTLE 완료: pid=%ld sid=%ld net=%ld paydate=%.8s", + h_pid, h_sid, h_net, h_pdate.arr); + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + +sql_error: + userlog("SETTLE SQL 오류: %.70s (sqlcode=%ld)", + sqlca.sqlerrm.sqlerrmc, sqlca.sqlcode); + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL ROLLBACK WORK; + Fchg(fb, ST_RC, 0, (char *)"E900", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; +} diff --git a/legacy-oracle/proc/ST_MDR.pc b/legacy-oracle/proc/ST_MDR.pc new file mode 100644 index 0000000..709106f --- /dev/null +++ b/legacy-oracle/proc/ST_MDR.pc @@ -0,0 +1,157 @@ +/*==========================================================================*/ +/* ST_MDR.pc - MDR 수수료 원장기표 온라인 서비스 (As-Is 원본, Oracle Pro*C)*/ +/*--------------------------------------------------------------------------*/ +/* 대상 : BC 매입시스템 / Tuxedo ATMI 서비스 ST_MDR */ +/* DBMS : Oracle 19c 전처리 : Pro*C/C++ (proc) */ +/* 기능 : 매입건 MDR 수수료를 계산·검증하여 원장(ledger)에 기표한다. */ +/* ※ 이 파일은 **As-Is 역사 원본**이다. 현행 오픈스택 이식본은 */ +/* app/src/st/svc/ST_MDR.pgc (ECPG/PostgreSQL). 컴파일 제외. */ +/*--------------------------------------------------------------------------*/ +/* 변경이력 */ +/* 2000.03.21 PARK J.S 최초작성 (MDR 원장기표) */ +/* 2007.11.05 HAN Y.J 가맹점 요율(mdr_bps) 마스터 연동, DECODE 분기 */ +/* 2013.06.30 LEE S.W 부가세 분리기표, 복식부기 차대변 연계 */ +/* 2019.09.01 CHOI H.W Oracle 11g -> 19c, 시퀀스 채번 표준화 */ +/*==========================================================================*/ +#include +#include +#include +#include +#include + +EXEC SQL INCLUDE sqlca; +EXEC SQL INCLUDE oraca; +EXEC ORACLE OPTION (ORACA=YES); + +#include "stfld.h" + +EXEC SQL BEGIN DECLARE SECTION; + VARCHAR h_merch[64]; + VARCHAR h_bizdate[9]; /* YYYYMMDD */ + VARCHAR h_pstat[2]; + long h_pid; + long h_sid; + long h_amount; + long h_fee; + long h_vat; + long h_net; + long h_mdr_bps; + long h_led; + long h_dup; +EXEC SQL END DECLARE SECTION; + +/*--------------------------------------------------------------------------*/ +/* ST_MDR - Tuxedo 서비스 엔트리 */ +/*--------------------------------------------------------------------------*/ +void ST_MDR(TPSVCINFO *rqst) +{ + FBFR32 *fb = (FBFR32 *)rqst->data; + char merch[64], bizdate[9]; + + userlog("ST_MDR 진입: MDR 수수료 원장기표"); + EXEC SQL WHENEVER SQLERROR GOTO sql_error; + + /* ---- 1) 요청 FML 파싱 -------------------------------------------- */ + h_pid = Fvall(fb, ST_PURCHASE_ID, 0); + h_sid = Fvall(fb, ST_SETTLE_ID, 0); + h_fee = Fvall(fb, ST_FEE, 0); + Fget(fb, ST_BIZDATE, 0, bizdate, (FLDLEN32 *)NULL); + strcpy((char *)h_bizdate.arr, bizdate); h_bizdate.len = strlen(bizdate); + if (h_bizdate.len == 0) { + EXEC SQL SELECT TO_CHAR(SYSDATE,'YYYYMMDD') INTO :h_bizdate FROM DUAL; + } + if (h_pid <= 0) { + userlog("ST_MDR: 매입 ID 필수 pid=%ld", h_pid); + Fchg(fb, ST_RC, 0, (char *)"E802", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 2) 매입원장 조회 (금액/가맹점/상태 확보, NVL 널가드) --------- */ + EXEC SQL + SELECT NVL(p.amount, 0), + NVL(p.merchant_id, ' '), + NVL(p.status, 'A') + INTO :h_amount, :h_merch, :h_pstat + FROM purchase p + WHERE p.purchase_id = :h_pid; + if (sqlca.sqlcode == 1403) { + /* 체인 내 신규매입(미커밋)은 전문값을 사용 */ + h_amount = Fvall(fb, ST_AMOUNT, 0); + Fget(fb, ST_MERCHANT, 0, merch, (FLDLEN32 *)NULL); + strcpy((char *)h_merch.arr, merch); h_merch.len = strlen(merch); + userlog("ST_MDR: 미커밋 매입 pid=%ld — 전문값 사용", h_pid); + } + + /* ---- 3) 요율조회 및 수수료 산출 (요청 fee 우선, 없으면 재계산) --- */ + EXEC SQL + SELECT NVL(m.mdr_bps, 25), + DECODE(NVL(m.status,'N'), 'A', 0, 1) + INTO :h_mdr_bps, :h_dup + FROM merchant m + WHERE m.merchant_id = :h_merch; + if (h_fee <= 0) h_fee = ROUND(h_amount * h_mdr_bps / 10000); + h_vat = ROUND(h_fee / 10); /* 수수료 부가세 10% 분리기표 */ + h_net = h_amount - h_fee - h_vat; + + /* ---- 4) 중복기표 방지: 동일 매입 MDR 기존 원장 존재여부 --------- */ + EXEC SQL + SELECT COUNT(*) INTO :h_dup + FROM ledger + WHERE purchase_id = :h_pid + AND entry_type = 'MDR'; + if (h_dup > 0) { + userlog("ST_MDR: MDR 기표 이미 존재 pid=%ld — 스킵", h_pid); + Fchg(fb, ST_STATUS, 0, (char *)"D", 0L); + EXEC SQL COMMIT WORK; + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + } + + /* ---- 5) 원장 기표: 시퀀스 채번 (.NEXTVAL) 후 MDR/VAT 2건 적재 --- */ + EXEC SQL SELECT ledger_seq.NEXTVAL INTO :h_led FROM DUAL; + EXEC SQL + INSERT INTO ledger + (ledger_id, purchase_id, entry_type, amount, biz_date, posted_at) + VALUES (:h_led, :h_pid, 'MDR', :h_fee, + TO_DATE(:h_bizdate,'YYYYMMDD'), SYSDATE); + EXEC SQL + INSERT INTO ledger + (ledger_id, purchase_id, entry_type, amount, biz_date, posted_at) + VALUES (ledger_seq.NEXTVAL, :h_pid, 'VAT', :h_vat, + TO_DATE(:h_bizdate,'YYYYMMDD'), SYSDATE); + + /* ---- 6) 복식부기 전표 반영 (PL/SQL 원장 패키지) ----------------- */ + EXEC SQL EXECUTE + BEGIN + pkg_ledger.post_voucher(:h_pid, 'MDR', :h_fee, :h_vat, :h_bizdate); + END; + END-EXEC; + + /* ---- 7) 정산연계: 수수료-원장반영 (LG_SETTLEPOST PL/SQL) -------- */ + EXEC SQL EXECUTE + BEGIN + pkg_settle.settle_post(:h_pid, :h_sid, :h_fee, :h_net); + END; + END-EXEC; + + EXEC SQL COMMIT WORK; + + Fchg(fb, ST_LEDGER_ID, 0, (char *)&h_led, 0L); + Fchg(fb, ST_FEE, 0, (char *)&h_fee, 0L); + Fchg(fb, ST_NET, 0, (char *)&h_net, 0L); + Fchg(fb, ST_STATUS, 0, (char *)"S", 0L); + userlog("ST_MDR 완료: pid=%ld ledger=%ld fee=%ld vat=%ld net=%ld", + h_pid, h_led, h_fee, h_vat, h_net); + tpreturn(TPSUCCESS, 0, (char *)fb, 0L, 0L); + return; + +sql_error: + userlog("ST_MDR SQL 오류: %.70s (sqlcode=%ld)", + sqlca.sqlerrm.sqlerrmc, sqlca.sqlcode); + EXEC SQL WHENEVER SQLERROR CONTINUE; + EXEC SQL ROLLBACK WORK; + Fchg(fb, ST_RC, 0, (char *)"E900", 0L); + tpreturn(TPFAIL, 0, (char *)fb, 0L, 0L); + return; +} diff --git a/tools/gen_metrics.py b/tools/gen_metrics.py index 97ddfa2..409ad02 100644 --- a/tools/gen_metrics.py +++ b/tools/gen_metrics.py @@ -142,7 +142,34 @@ LEGACY = { "embedded_sql_density": round(sh_count(pgc, r"EXEC SQL") / max(1, len(pgc)), 1), } -data = {"SCALE": SCALE, "HAZARDS": HAZARDS, "TRACE": TRACE, "LEGACY": LEGACY} +# ── Oracle 계보 (As-Is 원본 legacy-oracle/ 실측) ───────────────────────────── +ora_proc = glob.glob(os.path.join(ROOT, "legacy-oracle", "proc", "*.pc")) +ora_plsql = glob.glob(os.path.join(ROOT, "legacy-oracle", "plsql", "*.sql")) +ora_files = ora_proc + ora_plsql +def ora_hits(pat): return sh_count(ora_files, pat) if ora_files else 0 +ORACLE = { + "asis_files": len(ora_files), + "asis_proc": len(ora_proc), + "asis_plsql": len(ora_plsql), + "asis_loc": loc(ora_files) if ora_files else 0, + # As-Is 원본에서 실측한 Oracle 방언 (현행 이관에서 옮긴 것들) + "dialects": [ + {"t": "PL/SQL 패키지 호출 (pkg_x.proc)", "n": ora_hits(r"\bpkg_[a-z_]+\.")}, + {"t": "NVL / NVL2 널가드", "n": ora_hits(r"\bNVL2?\s*\(")}, + {"t": "DECODE 다분기", "n": ora_hits(r"\bDECODE\s*\(")}, + {"t": "SYSDATE / SYSTIMESTAMP", "n": ora_hits(r"\bSYS(DATE|TIMESTAMP)\b")}, + {"t": "FROM DUAL", "n": ora_hits(r"\bFROM\s+DUAL\b")}, + {"t": "시퀀스 .NEXTVAL", "n": ora_hits(r"\.NEXTVAL\b")}, + {"t": "ROWNUM 의사컬럼", "n": ora_hits(r"\bROWNUM\b")}, + {"t": "구식 외부조인 (+)", "n": ora_hits(r"\(\+\)")}, + {"t": "CONNECT BY 계층질의", "n": ora_hits(r"\bCONNECT\s+BY\b")}, + {"t": "WHENEVER SQLERROR 라벨분기", "n": ora_hits(r"WHENEVER\s+SQLERROR")}, + {"t": "TO_DATE / TO_CHAR 포맷", "n": ora_hits(r"\bTO_(DATE|CHAR)\s*\(")}, + {"t": "RAISE_APPLICATION_ERROR", "n": ora_hits(r"RAISE_APPLICATION_ERROR")}, + ], +} + +data = {"SCALE": SCALE, "HAZARDS": HAZARDS, "TRACE": TRACE, "LEGACY": LEGACY, "ORACLE": ORACLE} payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")) with open(OUT_JS, "w", encoding="utf-8") as w: w.write("/* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */\n") @@ -165,3 +192,6 @@ for t in TRACE: print(" 레거시: 카피북 %d(최대공유 %d×%s) 매크로 %d sqlca분기 %d EXEC밀도 %.1f/파일" % (LEGACY["copybook_total"], LEGACY["copybook_shared_max"], LEGACY["copybook_shared_name"], LEGACY["macro_defs"], LEGACY["sqlca_checks"], LEGACY["embedded_sql_density"])) +print(" Oracle As-Is: 원본 %d파일(proc %d/plsql %d) %dLOC · 방언히트 %d종" + % (ORACLE["asis_files"], ORACLE["asis_proc"], ORACLE["asis_plsql"], ORACLE["asis_loc"], + sum(1 for d in ORACLE["dialects"] if d["n"] > 0)))